summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorDirk Sierd de Vries <contact@lecapuchon.nl>2026-08-09 19:22:55 +0200
committerDirk Sierd de Vries <contact@lecapuchon.nl>2026-08-09 19:50:05 +0200
commitf9407d9cc8eb2edb4ab6a29a117b3ed11b4b8080 (patch)
tree9de2af5f6b43e9e0f40b2d6eede4f19a304f5c4c /app
parent7971690ac82fc5a0f3e547a8c2f3f1acfa5608ab (diff)
downloadSidePhOnelauncher-f9407d9cc8eb2edb4ab6a29a117b3ed11b4b8080.tar.gz
SidePhOnelauncher-f9407d9cc8eb2edb4ab6a29a117b3ed11b4b8080.tar.bz2
SidePhOnelauncher-f9407d9cc8eb2edb4ab6a29a117b3ed11b4b8080.zip
Add subtle notification dots to home app buttons
Diffstat (limited to 'app')
-rw-r--r--app/src/main/AndroidManifest.xml10
-rw-r--r--app/src/main/java/app/olauncher/helper/HomeNotificationListenerService.kt27
-rw-r--r--app/src/main/java/app/olauncher/helper/NotificationDotRepository.kt58
-rw-r--r--app/src/main/java/app/olauncher/ui/HomeAppTextView.kt51
-rw-r--r--app/src/main/java/app/olauncher/ui/HomeFragment.kt72
-rw-r--r--app/src/main/res/layout/fragment_home.xml36
6 files changed, 215 insertions, 39 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 91aa7af..d90ae12 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -102,6 +102,16 @@
</intent-filter>
</service>
+ <service
+ android:name=".helper.HomeNotificationListenerService"
+ android:exported="false"
+ android:label="@string/app_name"
+ android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+ <intent-filter>
+ <action android:name="android.service.notification.NotificationListenerService" />
+ </intent-filter>
+ </service>
+
</application>
</manifest> \ No newline at end of file
diff --git a/app/src/main/java/app/olauncher/helper/HomeNotificationListenerService.kt b/app/src/main/java/app/olauncher/helper/HomeNotificationListenerService.kt
new file mode 100644
index 0000000..59facf8
--- /dev/null
+++ b/app/src/main/java/app/olauncher/helper/HomeNotificationListenerService.kt
@@ -0,0 +1,27 @@
+package app.olauncher.helper
+
+import android.service.notification.NotificationListenerService
+import android.service.notification.StatusBarNotification
+
+class HomeNotificationListenerService : NotificationListenerService() {
+
+ override fun onListenerConnected() {
+ super.onListenerConnected()
+ NotificationDotRepository.update(activeNotifications)
+ }
+
+ override fun onNotificationPosted(sbn: StatusBarNotification?) {
+ super.onNotificationPosted(sbn)
+ NotificationDotRepository.update(activeNotifications)
+ }
+
+ override fun onNotificationRemoved(sbn: StatusBarNotification?) {
+ super.onNotificationRemoved(sbn)
+ NotificationDotRepository.update(activeNotifications)
+ }
+
+ override fun onListenerDisconnected() {
+ NotificationDotRepository.clear()
+ super.onListenerDisconnected()
+ }
+}
diff --git a/app/src/main/java/app/olauncher/helper/NotificationDotRepository.kt b/app/src/main/java/app/olauncher/helper/NotificationDotRepository.kt
new file mode 100644
index 0000000..0040ea5
--- /dev/null
+++ b/app/src/main/java/app/olauncher/helper/NotificationDotRepository.kt
@@ -0,0 +1,58 @@
+package app.olauncher.helper
+
+import android.app.Notification
+import android.service.notification.StatusBarNotification
+import java.util.concurrent.CopyOnWriteArraySet
+
+object NotificationDotRepository {
+ private val listeners = CopyOnWriteArraySet<() -> Unit>()
+
+ @Volatile
+ private var packagesWithNotifications: Set<String> = emptySet()
+
+ fun addListener(listener: () -> Unit) {
+ listeners += listener
+ }
+
+ fun removeListener(listener: () -> Unit) {
+ listeners -= listener
+ }
+
+ fun hasActiveNotification(packageName: String): Boolean {
+ if (packageName.isBlank()) return false
+ return packageName in packagesWithNotifications
+ }
+
+ fun update(activeNotifications: Array<StatusBarNotification>?) {
+ val nextPackages = activeNotifications
+ ?.asSequence()
+ ?.filterNot(::shouldIgnore)
+ ?.map { it.packageName }
+ ?.filter { it.isNotBlank() }
+ ?.toSet()
+ ?: emptySet()
+
+ if (nextPackages == packagesWithNotifications) return
+ packagesWithNotifications = nextPackages
+ listeners.forEach { it() }
+ }
+
+ fun clear() {
+ if (packagesWithNotifications.isEmpty()) return
+ packagesWithNotifications = emptySet()
+ listeners.forEach { it() }
+ }
+
+ private fun shouldIgnore(sbn: StatusBarNotification): Boolean {
+ val notification = sbn.notification
+ val flags = notification.flags
+ val category = notification.category
+ val isGroupSummary = flags and Notification.FLAG_GROUP_SUMMARY != 0
+ val isForegroundService = flags and Notification.FLAG_FOREGROUND_SERVICE != 0
+ val isOngoing = flags and Notification.FLAG_ONGOING_EVENT != 0
+ val isPersistent = !sbn.isClearable
+ val isTransport = category == Notification.CATEGORY_TRANSPORT
+ val isService = category == Notification.CATEGORY_SERVICE
+ return isGroupSummary || isForegroundService || isOngoing || isPersistent || isTransport || isService
+ }
+}
diff --git a/app/src/main/java/app/olauncher/ui/HomeAppTextView.kt b/app/src/main/java/app/olauncher/ui/HomeAppTextView.kt
new file mode 100644
index 0000000..144b6b9
--- /dev/null
+++ b/app/src/main/java/app/olauncher/ui/HomeAppTextView.kt
@@ -0,0 +1,51 @@
+package app.olauncher.ui
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.widget.TextView
+import app.olauncher.helper.dpToPx
+
+class HomeAppTextView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = android.R.attr.textViewStyle,
+) : TextView(context, attrs, defStyleAttr) {
+
+ private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = Color.WHITE
+ alpha = 220
+ style = Paint.Style.FILL
+ }
+
+ private val dotRadiusPx = 3.dpToPx().toFloat()
+ private val reservedSideSpacePx = 14.dpToPx()
+
+ private var showNotificationDot = false
+
+ init {
+ setPaddingRelative(
+ paddingStart + reservedSideSpacePx,
+ paddingTop,
+ paddingEnd + reservedSideSpacePx,
+ paddingBottom,
+ )
+ }
+
+ fun setShowNotificationDot(show: Boolean) {
+ if (showNotificationDot == show) return
+ showNotificationDot = show
+ invalidate()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ if (!showNotificationDot) return
+
+ val cx = width - reservedSideSpacePx / 2f
+ val cy = height / 2f
+ canvas.drawCircle(cx, cy, dotRadiusPx, dotPaint)
+ }
+}
diff --git a/app/src/main/java/app/olauncher/ui/HomeFragment.kt b/app/src/main/java/app/olauncher/ui/HomeFragment.kt
index 535002c..1d14c61 100644
--- a/app/src/main/java/app/olauncher/ui/HomeFragment.kt
+++ b/app/src/main/java/app/olauncher/ui/HomeFragment.kt
@@ -32,6 +32,7 @@ import app.olauncher.data.AppModel
import app.olauncher.data.Constants
import app.olauncher.data.Prefs
import app.olauncher.databinding.FragmentHomeBinding
+import app.olauncher.helper.NotificationDotRepository
import app.olauncher.helper.appUsagePermissionGranted
import app.olauncher.helper.dpToPx
import app.olauncher.helper.expandNotificationDrawer
@@ -62,6 +63,11 @@ class HomeFragment : Fragment(), View.OnClickListener, View.OnLongClickListener
private var dpadLeftKeyDownTime = 0L
private var dpadRightKeyDownTime = 0L
+ private val notificationDotListener: () -> Unit = {
+ activity?.runOnUiThread {
+ if (_binding != null) updateHomeNotificationDots()
+ }
+ }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
@@ -86,6 +92,7 @@ class HomeFragment : Fragment(), View.OnClickListener, View.OnLongClickListener
override fun onResume() {
super.onResume()
+ NotificationDotRepository.addListener(notificationDotListener)
dpadLeftKeyDownTime = 0L
dpadRightKeyDownTime = 0L
populateHomeScreen(false)
@@ -386,14 +393,7 @@ class HomeFragment : Fragment(), View.OnClickListener, View.OnLongClickListener
val verticalGravity = if (prefs.homeBottomAlignment) Gravity.BOTTOM else Gravity.CENTER_VERTICAL
binding.homeAppsLayout.gravity = horizontalGravity or verticalGravity
binding.dateTimeLayout.gravity = horizontalGravity
- binding.homeApp1.gravity = horizontalGravity
- binding.homeApp2.gravity = horizontalGravity
- binding.homeApp3.gravity = horizontalGravity
- binding.homeApp4.gravity = horizontalGravity
- binding.homeApp5.gravity = horizontalGravity
- binding.homeApp6.gravity = horizontalGravity
- binding.homeApp7.gravity = horizontalGravity
- binding.homeApp8.gravity = horizontalGravity
+ homeAppTextViews().forEach { it.gravity = horizontalGravity }
}
private fun populateDateTime() {
@@ -452,56 +452,56 @@ class HomeFragment : Fragment(), View.OnClickListener, View.OnLongClickListener
if (homeAppsNum == 0) return
binding.homeApp1.visibility = View.VISIBLE
- if (!setHomeAppText(binding.homeApp1, prefs.appName1, prefs.appPackage1, prefs.appUser1, prefs.isShortcut1, prefs.shortcutId1)) {
+ if (!setHomeAppText(binding.homeApp1 as TextView, prefs.appName1, prefs.appPackage1, prefs.appUser1, prefs.isShortcut1, prefs.shortcutId1)) {
prefs.appName1 = ""
prefs.appPackage1 = ""
}
if (homeAppsNum == 1) return
binding.homeApp2.visibility = View.VISIBLE
- if (!setHomeAppText(binding.homeApp2, prefs.appName2, prefs.appPackage2, prefs.appUser2, prefs.isShortcut2, prefs.shortcutId2)) {
+ if (!setHomeAppText(binding.homeApp2 as TextView, prefs.appName2, prefs.appPackage2, prefs.appUser2, prefs.isShortcut2, prefs.shortcutId2)) {
prefs.appName2 = ""
prefs.appPackage2 = ""
}
if (homeAppsNum == 2) return
binding.homeApp3.visibility = View.VISIBLE
- if (!setHomeAppText(binding.homeApp3, prefs.appName3, prefs.appPackage3, prefs.appUser3, prefs.isShortcut3, prefs.shortcutId3)) {
+ if (!setHomeAppText(binding.homeApp3 as TextView, prefs.appName3, prefs.appPackage3, prefs.appUser3, prefs.isShortcut3, prefs.shortcutId3)) {
prefs.appName3 = ""
prefs.appPackage3 = ""
}
if (homeAppsNum == 3) return
binding.homeApp4.visibility = View.VISIBLE
- if (!setHomeAppText(binding.homeApp4, prefs.appName4, prefs.appPackage4, prefs.appUser4, prefs.isShortcut4, prefs.shortcutId4)) {
+ if (!setHomeAppText(binding.homeApp4 as TextView, prefs.appName4, prefs.appPackage4, prefs.appUser4, prefs.isShortcut4, prefs.shortcutId4)) {
prefs.appName4 = ""
prefs.appPackage4 = ""
}
if (homeAppsNum == 4) return
binding.homeApp5.visibility = View.VISIBLE
- if (!setHomeAppText(binding.homeApp5, prefs.appName5, prefs.appPackage5, prefs.appUser5, prefs.isShortcut5, prefs.shortcutId5)) {
+ if (!setHomeAppText(binding.homeApp5 as TextView, prefs.appName5, prefs.appPackage5, prefs.appUser5, prefs.isShortcut5, prefs.shortcutId5)) {
prefs.appName5 = ""
prefs.appPackage5 = ""
}
if (homeAppsNum == 5) return
binding.homeApp6.visibility = View.VISIBLE
- if (!setHomeAppText(binding.homeApp6, prefs.appName6, prefs.appPackage6, prefs.appUser6, prefs.isShortcut6, prefs.shortcutId6)) {
+ if (!setHomeAppText(binding.homeApp6 as TextView, prefs.appName6, prefs.appPackage6, prefs.appUser6, prefs.isShortcut6, prefs.shortcutId6)) {
prefs.appName6 = ""
prefs.appPackage6 = ""
}
if (homeAppsNum == 6) return
binding.homeApp7.visibility = View.VISIBLE
- if (!setHomeAppText(binding.homeApp7, prefs.appName7, prefs.appPackage7, prefs.appUser7, prefs.isShortcut7, prefs.shortcutId7)) {
+ if (!setHomeAppText(binding.homeApp7 as TextView, prefs.appName7, prefs.appPackage7, prefs.appUser7, prefs.isShortcut7, prefs.shortcutId7)) {
prefs.appName7 = ""
prefs.appPackage7 = ""
}
if (homeAppsNum == 7) return
binding.homeApp8.visibility = View.VISIBLE
- if (!setHomeAppText(binding.homeApp8, prefs.appName8, prefs.appPackage8, prefs.appUser8, prefs.isShortcut8, prefs.shortcutId8)) {
+ if (!setHomeAppText(binding.homeApp8 as TextView, prefs.appName8, prefs.appPackage8, prefs.appUser8, prefs.isShortcut8, prefs.shortcutId8)) {
prefs.appName8 = ""
prefs.appPackage8 = ""
}
@@ -528,25 +528,60 @@ class HomeFragment : Fragment(), View.OnClickListener, View.OnLongClickListener
val shortcuts = launcherApps.getShortcuts(query, userHandle)
if (shortcuts?.any { it.id == shortcutId } == true) {
textView.text = appName
+ updateHomeAppDot(textView, packageName)
return true
}
textView.text = ""
+ updateHomeAppDot(textView, null)
return false
} catch (e: Exception) {
e.printStackTrace()
textView.text = ""
+ updateHomeAppDot(textView, null)
return false
}
}
if (isPackageInstalled(requireContext(), packageName, userString)) {
textView.text = appName
+ updateHomeAppDot(textView, packageName)
return true
}
textView.text = ""
+ updateHomeAppDot(textView, null)
return false
}
+ private fun updateHomeNotificationDots() {
+ updateHomeAppDot(binding.homeApp1 as TextView, prefs.appPackage1)
+ updateHomeAppDot(binding.homeApp2 as TextView, prefs.appPackage2)
+ updateHomeAppDot(binding.homeApp3 as TextView, prefs.appPackage3)
+ updateHomeAppDot(binding.homeApp4 as TextView, prefs.appPackage4)
+ updateHomeAppDot(binding.homeApp5 as TextView, prefs.appPackage5)
+ updateHomeAppDot(binding.homeApp6 as TextView, prefs.appPackage6)
+ updateHomeAppDot(binding.homeApp7 as TextView, prefs.appPackage7)
+ updateHomeAppDot(binding.homeApp8 as TextView, prefs.appPackage8)
+ }
+
+ private fun homeAppTextViews(): List<TextView> {
+ return listOf(
+ binding.homeApp1 as TextView,
+ binding.homeApp2 as TextView,
+ binding.homeApp3 as TextView,
+ binding.homeApp4 as TextView,
+ binding.homeApp5 as TextView,
+ binding.homeApp6 as TextView,
+ binding.homeApp7 as TextView,
+ binding.homeApp8 as TextView,
+ )
+ }
+
+ private fun updateHomeAppDot(textView: TextView, packageName: String?) {
+ (textView as? HomeAppTextView)?.setShowNotificationDot(
+ packageName.isNullOrBlank().not() && NotificationDotRepository.hasActiveNotification(packageName)
+ )
+ }
+
private fun restoreHomeAppFocusability() {
listOf(
binding.homeApp1,
@@ -867,6 +902,11 @@ class HomeFragment : Fragment(), View.OnClickListener, View.OnLongClickListener
}
}
+ override fun onPause() {
+ NotificationDotRepository.removeListener(notificationDotListener)
+ super.onPause()
+ }
+
override fun onDestroyView() {
super.onDestroyView()
_binding = null
diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml
index 7de8a4a..0f474c2 100644
--- a/app/src/main/res/layout/fragment_home.xml
+++ b/app/src/main/res/layout/fragment_home.xml
@@ -8,22 +8,12 @@
android:orientation="vertical"
tools:context=".ui.HomeFragment">
- <!-- Placeholder layout for locking screen-->
<FrameLayout
android:id="@+id/lock"
android:layout_width="match_parent"
android:layout_height="1dp"
android:contentDescription="@string/lock_layout_description" />
- <!-- Home button for recents feature disabled -->
- <!-- Placeholder layout for showing recent apps
- <FrameLayout
- android:id="@+id/recents"
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:contentDescription="@string/recents_layout_description" /> -->
-
- <!-- Clock and calendar-->
<LinearLayout
android:id="@+id/dateTimeLayout"
android:layout_width="match_parent"
@@ -41,6 +31,8 @@
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"
android:format12Hour="h:mm"
+ android:paddingStart="14dp"
+ android:paddingEnd="14dp"
android:textSize="@dimen/time_size"
tools:text="02:34" />
@@ -51,12 +43,12 @@
android:layout_height="wrap_content"
android:format12Hour="EEE, dd MMM"
android:format24Hour="EEE, dd MMM"
- android:paddingHorizontal="3dp"
+ android:paddingStart="7dp"
+ android:paddingEnd="7dp"
android:textSize="@dimen/date_size"
tools:text="Thu, 30 Dec" />
</LinearLayout>
- <!-- Screen time-->
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvScreenTime"
style="@style/TextSmall"
@@ -72,7 +64,6 @@
tools:text="2h 11m"
tools:visibility="visible" />
- <!-- Home apps-->
<LinearLayout
android:id="@+id/homeAppsLayout"
android:layout_width="match_parent"
@@ -83,7 +74,7 @@
android:paddingTop="112dp"
android:paddingBottom="48dp">
- <TextView
+ <app.olauncher.ui.HomeAppTextView
android:id="@+id/homeApp1"
style="@style/TextLarge"
android:layout_width="wrap_content"
@@ -96,7 +87,7 @@
android:visibility="gone"
tools:visibility="visible" />
- <TextView
+ <app.olauncher.ui.HomeAppTextView
android:id="@+id/homeApp2"
style="@style/TextLarge"
android:layout_width="wrap_content"
@@ -109,7 +100,7 @@
android:visibility="gone"
tools:visibility="visible" />
- <TextView
+ <app.olauncher.ui.HomeAppTextView
android:id="@+id/homeApp3"
style="@style/TextLarge"
android:layout_width="wrap_content"
@@ -122,7 +113,7 @@
android:visibility="gone"
tools:visibility="visible" />
- <TextView
+ <app.olauncher.ui.HomeAppTextView
android:id="@+id/homeApp4"
style="@style/TextLarge"
android:layout_width="wrap_content"
@@ -135,7 +126,7 @@
android:visibility="gone"
tools:visibility="visible" />
- <TextView
+ <app.olauncher.ui.HomeAppTextView
android:id="@+id/homeApp5"
style="@style/TextLarge"
android:layout_width="wrap_content"
@@ -148,7 +139,7 @@
android:visibility="gone"
tools:visibility="visible" />
- <TextView
+ <app.olauncher.ui.HomeAppTextView
android:id="@+id/homeApp6"
style="@style/TextLarge"
android:layout_width="wrap_content"
@@ -161,7 +152,7 @@
android:visibility="gone"
tools:visibility="visible" />
- <TextView
+ <app.olauncher.ui.HomeAppTextView
android:id="@+id/homeApp7"
style="@style/TextLarge"
android:layout_width="wrap_content"
@@ -174,7 +165,7 @@
android:visibility="gone"
tools:visibility="visible" />
- <TextView
+ <app.olauncher.ui.HomeAppTextView
android:id="@+id/homeApp8"
style="@style/TextLarge"
android:layout_width="wrap_content"
@@ -202,7 +193,6 @@
android:visibility="gone"
tools:visibility="visible" />
- <!-- Set default launcher -->
<TextView
android:id="@+id/setDefaultLauncher"
style="@style/TextMedium"
@@ -215,4 +205,4 @@
android:maxLines="1"
android:text="@string/set_as_default_launcher_u"
android:visibility="gone" />
-</FrameLayout> \ No newline at end of file
+</FrameLayout>