feat: notification dots on app icons
New NotificationBadgeService (NotificationListenerService) tracks pending, dismissible notifications per app. Home, favorite, and drawer icons get a red dot in the top-right corner when the app has active notifications. Settings: 'Notification Dots' toggle under Display features; enabling it opens the system notification-access screen when permission is missing. Dots update live via a StateFlow observed by the fragments.
This commit is contained in:
@@ -103,6 +103,15 @@
|
|||||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
<service
|
||||||
|
android:name=".service.NotificationBadgeService"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/notification_badge_service_label"
|
||||||
|
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.notification.NotificationListenerService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
|
|||||||
@@ -198,6 +198,10 @@ class PreferenceHelper @Inject constructor(@ApplicationContext context: Context)
|
|||||||
get() = prefs.getBoolean(Constants.DISABLE_ANIMATIONS, false)
|
get() = prefs.getBoolean(Constants.DISABLE_ANIMATIONS, false)
|
||||||
set(value) = prefs.edit().putBoolean(Constants.DISABLE_ANIMATIONS, value).apply()
|
set(value) = prefs.edit().putBoolean(Constants.DISABLE_ANIMATIONS, value).apply()
|
||||||
|
|
||||||
|
var showNotificationDots: Boolean
|
||||||
|
get() = prefs.getBoolean(Constants.SHOW_NOTIFICATION_DOTS, false)
|
||||||
|
set(value) = prefs.edit().putBoolean(Constants.SHOW_NOTIFICATION_DOTS, value).apply()
|
||||||
|
|
||||||
var searchEngines: Constants.SearchEngines
|
var searchEngines: Constants.SearchEngines
|
||||||
get() {
|
get() {
|
||||||
return try {
|
return try {
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.github.droidworksstudio.launcher.service
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.service.notification.NotificationListenerService
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks pending (dismissible) notifications per app so the launcher can show
|
||||||
|
* a red dot on app icons.
|
||||||
|
*
|
||||||
|
* The user must grant notification access in the system settings
|
||||||
|
* (Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS) before this service
|
||||||
|
* receives any events. Android rebinds it automatically after every boot.
|
||||||
|
*/
|
||||||
|
class NotificationBadgeService : NotificationListenerService() {
|
||||||
|
|
||||||
|
/** Notification key -> "userId/packageName". Keyed to dedupe updates. */
|
||||||
|
private val keyToPackage = HashMap<String, String>()
|
||||||
|
|
||||||
|
override fun onListenerConnected() {
|
||||||
|
super.onListenerConnected()
|
||||||
|
synchronized(keyToPackage) {
|
||||||
|
keyToPackage.clear()
|
||||||
|
activeNotifications.forEach { sbn ->
|
||||||
|
if (isCounted(sbn)) keyToPackage[sbn.key] = userIdKey(sbn)
|
||||||
|
}
|
||||||
|
publish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||||
|
if (!isCounted(sbn)) return
|
||||||
|
synchronized(keyToPackage) {
|
||||||
|
// A posted notification with an existing key is just an update.
|
||||||
|
if (keyToPackage.containsKey(sbn.key)) return
|
||||||
|
keyToPackage[sbn.key] = userIdKey(sbn)
|
||||||
|
publish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNotificationRemoved(sbn: StatusBarNotification) {
|
||||||
|
synchronized(keyToPackage) {
|
||||||
|
if (keyToPackage.remove(sbn.key) != null) publish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Only swipe-away, non-ongoing notifications get a dot. */
|
||||||
|
private fun isCounted(sbn: StatusBarNotification): Boolean {
|
||||||
|
val notification = sbn.notification
|
||||||
|
if (!sbn.isClearable) return false
|
||||||
|
if (notification.flags and Notification.FLAG_ONGOING_EVENT != 0) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun userIdKey(sbn: StatusBarNotification) =
|
||||||
|
"${sbn.userId}/" + sbn.packageName
|
||||||
|
|
||||||
|
private fun publish() {
|
||||||
|
val counts = HashMap<String, Int>()
|
||||||
|
keyToPackage.values.forEach { key ->
|
||||||
|
counts[key] = (counts[key] ?: 0) + 1
|
||||||
|
}
|
||||||
|
notificationCounts.value = counts
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Active notification count per app, keyed by "userId/packageName". */
|
||||||
|
val notificationCounts: MutableStateFlow<Map<String, Int>> =
|
||||||
|
MutableStateFlow(emptyMap())
|
||||||
|
|
||||||
|
fun hasNotifications(userId: Int, packageName: String): Boolean =
|
||||||
|
notificationCounts.value["$userId/$packageName"]?.let { it > 0 } ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ import com.github.droidworksstudio.launcher.helper.PreferenceHelper
|
|||||||
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
|
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
|
||||||
import com.github.droidworksstudio.launcher.listener.OnSwipeTouchListener
|
import com.github.droidworksstudio.launcher.listener.OnSwipeTouchListener
|
||||||
import com.github.droidworksstudio.launcher.listener.ScrollEventListener
|
import com.github.droidworksstudio.launcher.listener.ScrollEventListener
|
||||||
|
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import com.github.droidworksstudio.launcher.ui.bottomsheetdialog.AppInfoBottomSheetFragment
|
import com.github.droidworksstudio.launcher.ui.bottomsheetdialog.AppInfoBottomSheetFragment
|
||||||
import com.github.droidworksstudio.launcher.utils.Constants
|
import com.github.droidworksstudio.launcher.utils.Constants
|
||||||
@@ -111,6 +112,7 @@ class DrawFragment : Fragment(),
|
|||||||
observeClickListener()
|
observeClickListener()
|
||||||
observeSwipeTouchListener()
|
observeSwipeTouchListener()
|
||||||
observeScrollTouchListener()
|
observeScrollTouchListener()
|
||||||
|
observeNotificationBadges()
|
||||||
|
|
||||||
// Initialize observation of drawer apps
|
// Initialize observation of drawer apps
|
||||||
observeDrawerApps()
|
observeDrawerApps()
|
||||||
@@ -166,6 +168,25 @@ class DrawFragment : Fragment(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeNotificationBadges() {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
NotificationBadgeService.notificationCounts.collect {
|
||||||
|
val recyclerView = binding.drawAdapter
|
||||||
|
for (i in 0 until recyclerView.childCount) {
|
||||||
|
val holder = recyclerView.getChildViewHolder(recyclerView.getChildAt(i))
|
||||||
|
if (holder is DrawViewHolder) {
|
||||||
|
val position = holder.bindingAdapterPosition
|
||||||
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
|
holder.bind(drawAdapter.currentList[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
private fun observeDrawerApps() {
|
private fun observeDrawerApps() {
|
||||||
// Start comparing installed app information
|
// Start comparing installed app information
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import com.github.droidworksstudio.launcher.data.entities.AppInfo
|
|||||||
import com.github.droidworksstudio.launcher.databinding.ItemDrawBinding
|
import com.github.droidworksstudio.launcher.databinding.ItemDrawBinding
|
||||||
import com.github.droidworksstudio.launcher.helper.PreferenceHelper
|
import com.github.droidworksstudio.launcher.helper.PreferenceHelper
|
||||||
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
|
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
|
||||||
|
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
|
||||||
import com.github.droidworksstudio.launcher.utils.Constants
|
import com.github.droidworksstudio.launcher.utils.Constants
|
||||||
|
import com.github.droidworksstudio.launcher.utils.NotificationDotHelper
|
||||||
|
|
||||||
class DrawViewHolder(
|
class DrawViewHolder(
|
||||||
private val binding: ItemDrawBinding,
|
private val binding: ItemDrawBinding,
|
||||||
@@ -70,7 +72,20 @@ class DrawViewHolder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
appDrawIcon.layoutParams = layoutParams
|
appDrawIcon.layoutParams = layoutParams
|
||||||
appDrawIcon.setImageDrawable(appNewIcon ?: nonNullDrawable)
|
|
||||||
|
val baseIcon: Drawable = appNewIcon ?: nonNullDrawable
|
||||||
|
appDrawIcon.setImageDrawable(
|
||||||
|
if (preferenceHelper.showNotificationDots &&
|
||||||
|
NotificationBadgeService.hasNotifications(
|
||||||
|
appInfo.userHandle,
|
||||||
|
appInfo.packageName
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
NotificationDotHelper.withDot(itemView.context, baseIcon)
|
||||||
|
} else {
|
||||||
|
baseIcon
|
||||||
|
}
|
||||||
|
)
|
||||||
appDrawIcon.visibility = View.VISIBLE
|
appDrawIcon.visibility = View.VISIBLE
|
||||||
|
|
||||||
val parentLayout = appDrawName.parent as LinearLayoutCompat
|
val parentLayout = appDrawName.parent as LinearLayoutCompat
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import android.view.ViewGroup
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@@ -28,6 +30,8 @@ import com.github.droidworksstudio.launcher.helper.PreferenceHelper
|
|||||||
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
|
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
|
||||||
import com.github.droidworksstudio.launcher.listener.OnItemMoveListener
|
import com.github.droidworksstudio.launcher.listener.OnItemMoveListener
|
||||||
import com.github.droidworksstudio.launcher.listener.OnSwipeTouchListener
|
import com.github.droidworksstudio.launcher.listener.OnSwipeTouchListener
|
||||||
|
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
|
||||||
|
import com.github.droidworksstudio.launcher.ui.favorite.FavoriteViewHolder
|
||||||
import com.github.droidworksstudio.launcher.viewmodel.AppViewModel
|
import com.github.droidworksstudio.launcher.viewmodel.AppViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -82,6 +86,7 @@ class FavoriteFragment : Fragment(),
|
|||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
observeFavorite()
|
observeFavorite()
|
||||||
observeHomeAppOrder()
|
observeHomeAppOrder()
|
||||||
|
observeNotificationBadges()
|
||||||
observeSwipeTouchListener()
|
observeSwipeTouchListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +133,25 @@ class FavoriteFragment : Fragment(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeNotificationBadges() {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
NotificationBadgeService.notificationCounts.collect {
|
||||||
|
val recyclerView = binding.favoriteAdapter
|
||||||
|
for (i in 0 until recyclerView.childCount) {
|
||||||
|
val holder = recyclerView.getChildViewHolder(recyclerView.getChildAt(i))
|
||||||
|
if (holder is FavoriteViewHolder) {
|
||||||
|
val position = holder.bindingAdapterPosition
|
||||||
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
|
holder.bind(favoriteAdapter.currentList[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun observeHomeAppOrder() {
|
private fun observeHomeAppOrder() {
|
||||||
binding.favoriteAdapter.adapter = favoriteAdapter
|
binding.favoriteAdapter.adapter = favoriteAdapter
|
||||||
val listener: OnItemMoveListener.OnItemActionListener = favoriteAdapter
|
val listener: OnItemMoveListener.OnItemActionListener = favoriteAdapter
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import com.github.droidworksstudio.launcher.data.entities.AppInfo
|
|||||||
import com.github.droidworksstudio.launcher.databinding.ItemFavoriteBinding
|
import com.github.droidworksstudio.launcher.databinding.ItemFavoriteBinding
|
||||||
import com.github.droidworksstudio.launcher.helper.PreferenceHelper
|
import com.github.droidworksstudio.launcher.helper.PreferenceHelper
|
||||||
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
|
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
|
||||||
|
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
|
||||||
|
import com.github.droidworksstudio.launcher.utils.NotificationDotHelper
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
class FavoriteViewHolder(
|
class FavoriteViewHolder(
|
||||||
@@ -48,7 +50,18 @@ class FavoriteViewHolder(
|
|||||||
if (preferenceHelper.showAppIcon) {
|
if (preferenceHelper.showAppIcon) {
|
||||||
val appIcon =
|
val appIcon =
|
||||||
binding.root.context.packageManager.getApplicationIcon(appInfo.packageName)
|
binding.root.context.packageManager.getApplicationIcon(appInfo.packageName)
|
||||||
appFavoriteLeftIcon.setImageDrawable(appIcon)
|
val baseIcon: android.graphics.drawable.Drawable =
|
||||||
|
if (preferenceHelper.showNotificationDots &&
|
||||||
|
NotificationBadgeService.hasNotifications(
|
||||||
|
appInfo.userHandle,
|
||||||
|
appInfo.packageName
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
NotificationDotHelper.withDot(binding.root.context, appIcon)
|
||||||
|
} else {
|
||||||
|
appIcon
|
||||||
|
}
|
||||||
|
appFavoriteLeftIcon.setImageDrawable(baseIcon)
|
||||||
appFavoriteLeftIcon.layoutParams.width =
|
appFavoriteLeftIcon.layoutParams.width =
|
||||||
preferenceHelper.appTextSize.toInt() * 3
|
preferenceHelper.appTextSize.toInt() * 3
|
||||||
appFavoriteLeftIcon.layoutParams.height =
|
appFavoriteLeftIcon.layoutParams.height =
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ import androidx.biometric.BiometricPrompt
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.NavOptions
|
import androidx.navigation.NavOptions
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
||||||
@@ -46,6 +48,7 @@ import com.github.droidworksstudio.launcher.helper.PreferenceHelper
|
|||||||
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
|
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
|
||||||
import com.github.droidworksstudio.launcher.listener.OnSwipeTouchListener
|
import com.github.droidworksstudio.launcher.listener.OnSwipeTouchListener
|
||||||
import com.github.droidworksstudio.launcher.listener.ScrollEventListener
|
import com.github.droidworksstudio.launcher.listener.ScrollEventListener
|
||||||
|
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
|
||||||
import com.github.droidworksstudio.launcher.ui.bottomsheetdialog.AppInfoBottomSheetFragment
|
import com.github.droidworksstudio.launcher.ui.bottomsheetdialog.AppInfoBottomSheetFragment
|
||||||
import com.github.droidworksstudio.launcher.utils.Constants
|
import com.github.droidworksstudio.launcher.utils.Constants
|
||||||
import com.github.droidworksstudio.launcher.viewmodel.AppViewModel
|
import com.github.droidworksstudio.launcher.viewmodel.AppViewModel
|
||||||
@@ -232,6 +235,25 @@ class HomeFragment : Fragment(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeNotificationBadges() {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||||
|
NotificationBadgeService.notificationCounts.collect {
|
||||||
|
val recyclerView = binding.appListAdapter
|
||||||
|
for (i in 0 until recyclerView.childCount) {
|
||||||
|
val holder = recyclerView.getChildViewHolder(recyclerView.getChildAt(i))
|
||||||
|
if (holder is HomeViewHolder) {
|
||||||
|
val position = holder.bindingAdapterPosition
|
||||||
|
if (position != androidx.recyclerview.widget.RecyclerView.NO_POSITION) {
|
||||||
|
holder.bind(homeAdapter.currentList[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility", "InflateParams")
|
@SuppressLint("ClickableViewAccessibility", "InflateParams")
|
||||||
private fun observeSwipeTouchListener() {
|
private fun observeSwipeTouchListener() {
|
||||||
binding.apply {
|
binding.apply {
|
||||||
@@ -584,6 +606,7 @@ class HomeFragment : Fragment(),
|
|||||||
binding.mainView.hideKeyboard()
|
binding.mainView.hideKeyboard()
|
||||||
observeUserInterfaceSettings()
|
observeUserInterfaceSettings()
|
||||||
observeFavoriteAppList()
|
observeFavoriteAppList()
|
||||||
|
observeNotificationBadges()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAppClicked(appInfo: AppInfo) {
|
override fun onAppClicked(appInfo: AppInfo) {
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import com.github.droidworksstudio.launcher.data.entities.AppInfo
|
|||||||
import com.github.droidworksstudio.launcher.databinding.ItemHomeBinding
|
import com.github.droidworksstudio.launcher.databinding.ItemHomeBinding
|
||||||
import com.github.droidworksstudio.launcher.helper.PreferenceHelper
|
import com.github.droidworksstudio.launcher.helper.PreferenceHelper
|
||||||
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
|
import com.github.droidworksstudio.launcher.listener.OnItemClickedListener
|
||||||
|
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
|
||||||
import com.github.droidworksstudio.launcher.utils.Constants
|
import com.github.droidworksstudio.launcher.utils.Constants
|
||||||
|
import com.github.droidworksstudio.launcher.utils.NotificationDotHelper
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class HomeViewHolder @Inject constructor(
|
class HomeViewHolder @Inject constructor(
|
||||||
@@ -70,7 +72,20 @@ class HomeViewHolder @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
appHomeIcon.layoutParams = layoutParams
|
appHomeIcon.layoutParams = layoutParams
|
||||||
appHomeIcon.setImageDrawable(appNewIcon ?: nonNullDrawable)
|
|
||||||
|
val baseIcon: Drawable = appNewIcon ?: nonNullDrawable
|
||||||
|
appHomeIcon.setImageDrawable(
|
||||||
|
if (preferenceHelper.showNotificationDots &&
|
||||||
|
NotificationBadgeService.hasNotifications(
|
||||||
|
appInfo.userHandle,
|
||||||
|
appInfo.packageName
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
NotificationDotHelper.withDot(itemView.context, baseIcon)
|
||||||
|
} else {
|
||||||
|
baseIcon
|
||||||
|
}
|
||||||
|
)
|
||||||
appHomeIcon.visibility = View.VISIBLE
|
appHomeIcon.visibility = View.VISIBLE
|
||||||
|
|
||||||
val parentLayout = appHomeName.parent as LinearLayoutCompat
|
val parentLayout = appHomeName.parent as LinearLayoutCompat
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.github.droidworksstudio.launcher.ui.settings
|
package com.github.droidworksstudio.launcher.ui.settings
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -18,6 +21,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.github.droidworksstudio.common.getAppNameFromPackageName
|
import com.github.droidworksstudio.common.getAppNameFromPackageName
|
||||||
|
import com.github.droidworksstudio.common.showLongToast
|
||||||
import com.github.droidworksstudio.launcher.R
|
import com.github.droidworksstudio.launcher.R
|
||||||
import com.github.droidworksstudio.launcher.databinding.FragmentSettingsFeaturesBinding
|
import com.github.droidworksstudio.launcher.databinding.FragmentSettingsFeaturesBinding
|
||||||
import com.github.droidworksstudio.launcher.helper.AppHelper
|
import com.github.droidworksstudio.launcher.helper.AppHelper
|
||||||
@@ -25,6 +29,7 @@ import com.github.droidworksstudio.launcher.helper.AppReloader
|
|||||||
import com.github.droidworksstudio.launcher.helper.PreferenceHelper
|
import com.github.droidworksstudio.launcher.helper.PreferenceHelper
|
||||||
import com.github.droidworksstudio.launcher.listener.ScrollEventListener
|
import com.github.droidworksstudio.launcher.listener.ScrollEventListener
|
||||||
import com.github.droidworksstudio.launcher.repository.AppInfoRepository
|
import com.github.droidworksstudio.launcher.repository.AppInfoRepository
|
||||||
|
import com.github.droidworksstudio.launcher.service.NotificationBadgeService
|
||||||
import com.github.droidworksstudio.launcher.utils.Constants
|
import com.github.droidworksstudio.launcher.utils.Constants
|
||||||
import com.github.droidworksstudio.launcher.viewmodel.PreferenceViewModel
|
import com.github.droidworksstudio.launcher.viewmodel.PreferenceViewModel
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
@@ -129,6 +134,7 @@ class SettingsFeaturesFragment : Fragment(),
|
|||||||
homeAlignmentBottomSwitchCompat.isChecked = preferenceHelper.homeAlignmentBottom
|
homeAlignmentBottomSwitchCompat.isChecked = preferenceHelper.homeAlignmentBottom
|
||||||
lockSettingsSwitchCompat.isChecked = preferenceHelper.settingsLock
|
lockSettingsSwitchCompat.isChecked = preferenceHelper.settingsLock
|
||||||
disableAnimationsSwitchCompat.isChecked = preferenceHelper.disableAnimations
|
disableAnimationsSwitchCompat.isChecked = preferenceHelper.disableAnimations
|
||||||
|
showNotificationDotsSwitchCompat.isChecked = preferenceHelper.showNotificationDots
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,10 +456,37 @@ class SettingsFeaturesFragment : Fragment(),
|
|||||||
val feedbackType = if (isChecked) "on" else "off"
|
val feedbackType = if (isChecked) "on" else "off"
|
||||||
appHelper.triggerHapticFeedback(context, feedbackType)
|
appHelper.triggerHapticFeedback(context, feedbackType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showNotificationDotsSwitchCompat.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
preferenceViewModel.setShowNotificationDots(isChecked)
|
||||||
|
val feedbackType = if (isChecked) "on" else "off"
|
||||||
|
appHelper.triggerHapticFeedback(context, feedbackType)
|
||||||
|
|
||||||
|
if (isChecked && !isNotificationAccessGranted()) {
|
||||||
|
openNotificationAccessSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isNotificationAccessGranted(): Boolean {
|
||||||
|
val componentName = ComponentName(requireContext(), NotificationBadgeService::class.java)
|
||||||
|
val notificationManager =
|
||||||
|
requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager
|
||||||
|
return notificationManager.isNotificationListenerAccessGranted(componentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openNotificationAccessSettings() {
|
||||||
|
try {
|
||||||
|
startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
|
||||||
|
} catch (_: Exception) {
|
||||||
|
requireContext().showLongToast(
|
||||||
|
getString(R.string.toast_cannot_open_notification_access_settings)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var swipeActionDialog: AlertDialog? = null
|
private var swipeActionDialog: AlertDialog? = null
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ object Constants {
|
|||||||
const val HOME_ALLIGNMENT_BOTTOM = "HOME_ALLIGNMENT_BOTTOM"
|
const val HOME_ALLIGNMENT_BOTTOM = "HOME_ALLIGNMENT_BOTTOM"
|
||||||
const val TOGGLE_SETTING_LOCK = "TOGGLE_SETTING_LOCK"
|
const val TOGGLE_SETTING_LOCK = "TOGGLE_SETTING_LOCK"
|
||||||
const val DISABLE_ANIMATIONS = "DISABLE_ANIMATIONS"
|
const val DISABLE_ANIMATIONS = "DISABLE_ANIMATIONS"
|
||||||
|
const val SHOW_NOTIFICATION_DOTS = "SHOW_NOTIFICATION_DOTS"
|
||||||
|
|
||||||
const val HOME_DATE_ALIGNMENT = "HOME_DATE_ALIGNMENT"
|
const val HOME_DATE_ALIGNMENT = "HOME_DATE_ALIGNMENT"
|
||||||
const val HOME_TIME_ALIGNMENT = "HOME_TIME_ALIGNMENT"
|
const val HOME_TIME_ALIGNMENT = "HOME_TIME_ALIGNMENT"
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.github.droidworksstudio.launcher.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import com.github.droidworksstudio.common.ColorIconsExtensions
|
||||||
|
|
||||||
|
object NotificationDotHelper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of [icon] with a red dot drawn in its top-right corner,
|
||||||
|
* surrounded by a thin white border so it stays visible on busy icons.
|
||||||
|
*/
|
||||||
|
fun withDot(context: Context, icon: Drawable): Drawable {
|
||||||
|
val source = ColorIconsExtensions.drawableToBitmap(icon)
|
||||||
|
val width = source.width
|
||||||
|
val height = source.height
|
||||||
|
if (width <= 0 || height <= 0) return icon
|
||||||
|
|
||||||
|
val dotRadius = (minOf(width, height) * 0.22f).coerceAtLeast(4f)
|
||||||
|
val centerX = width - dotRadius * 0.75f
|
||||||
|
val centerY = dotRadius * 0.75f
|
||||||
|
|
||||||
|
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
canvas.drawBitmap(source, 0f, 0f, null)
|
||||||
|
|
||||||
|
val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.RED
|
||||||
|
}
|
||||||
|
canvas.drawCircle(centerX, centerY, dotRadius, dotPaint)
|
||||||
|
|
||||||
|
val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = Color.WHITE
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = (dotRadius * 0.28f).coerceAtLeast(1.5f)
|
||||||
|
}
|
||||||
|
canvas.drawCircle(
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
dotRadius - borderPaint.strokeWidth / 2f,
|
||||||
|
borderPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
return BitmapDrawable(context.resources, bitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ class PreferenceViewModel @Inject constructor(
|
|||||||
private val autoKeyboardLiveData: MutableLiveData<Boolean> = MutableLiveData()
|
private val autoKeyboardLiveData: MutableLiveData<Boolean> = MutableLiveData()
|
||||||
private val lockSettingsLiveData: MutableLiveData<Boolean> = MutableLiveData()
|
private val lockSettingsLiveData: MutableLiveData<Boolean> = MutableLiveData()
|
||||||
private val disableAnimationsLiveData: MutableLiveData<Boolean> = MutableLiveData()
|
private val disableAnimationsLiveData: MutableLiveData<Boolean> = MutableLiveData()
|
||||||
|
private val showNotificationDotsLiveData: MutableLiveData<Boolean> = MutableLiveData()
|
||||||
private val appGroupPaddingSizeLiveData: MutableLiveData<Float> = MutableLiveData()
|
private val appGroupPaddingSizeLiveData: MutableLiveData<Float> = MutableLiveData()
|
||||||
private val appPaddingSizeLiveData: MutableLiveData<Float> = MutableLiveData()
|
private val appPaddingSizeLiveData: MutableLiveData<Float> = MutableLiveData()
|
||||||
|
|
||||||
@@ -297,6 +298,11 @@ class PreferenceViewModel @Inject constructor(
|
|||||||
disableAnimationsLiveData.postValue((preferenceHelper.disableAnimations))
|
disableAnimationsLiveData.postValue((preferenceHelper.disableAnimations))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setShowNotificationDots(showNotificationDots: Boolean) {
|
||||||
|
preferenceHelper.showNotificationDots = showNotificationDots
|
||||||
|
showNotificationDotsLiveData.postValue((preferenceHelper.showNotificationDots))
|
||||||
|
}
|
||||||
|
|
||||||
fun setAppLanguage(appLanguage: Constants.Language) {
|
fun setAppLanguage(appLanguage: Constants.Language) {
|
||||||
preferenceHelper.appLanguage = appLanguage
|
preferenceHelper.appLanguage = appLanguage
|
||||||
appLanguageLiveData.postValue((preferenceHelper.appLanguage))
|
appLanguageLiveData.postValue((preferenceHelper.appLanguage))
|
||||||
|
|||||||
@@ -238,6 +238,37 @@
|
|||||||
tools:ignore="TouchTargetSizeCheck" />
|
tools:ignore="TouchTargetSizeCheck" />
|
||||||
|
|
||||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
tools:ignore="MissingConstraints">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/showNotificationDots_text"
|
||||||
|
style="@style/TextDefaultStyle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="left|center"
|
||||||
|
android:text="@string/settings_display_notification_dots"
|
||||||
|
android:textSize="@dimen/text_large"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="RtlHardcoded" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/showNotificationDots_switchCompat"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:scaleX="0.7"
|
||||||
|
android:scaleY="0.8"
|
||||||
|
android:thumb="@drawable/shape_switch_thumb"
|
||||||
|
app:track="@drawable/selector_switch"
|
||||||
|
tools:ignore="TouchTargetSizeCheck" />
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
<androidx.appcompat.widget.LinearLayoutCompat
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
|||||||
@@ -110,6 +110,8 @@
|
|||||||
<string name="settings_search_from_start">Search From Start</string>
|
<string name="settings_search_from_start">Search From Start</string>
|
||||||
<string name="settings_display_lock_settings">Lock Settings</string>
|
<string name="settings_display_lock_settings">Lock Settings</string>
|
||||||
<string name="settings_display_disable_animations">Disable Animations</string>
|
<string name="settings_display_disable_animations">Disable Animations</string>
|
||||||
|
<string name="settings_display_notification_dots">Notification Dots</string>
|
||||||
|
<string name="toast_cannot_open_notification_access_settings">Cannot open notification access settings.</string>
|
||||||
|
|
||||||
<string name="settings_appearance_text_size_title">Size</string>
|
<string name="settings_appearance_text_size_title">Size</string>
|
||||||
<string name="settings_appearance_color_title">Color</string>
|
<string name="settings_appearance_color_title">Color</string>
|
||||||
@@ -200,6 +202,7 @@
|
|||||||
<string name="accessibility_settings_disable">Disable</string>
|
<string name="accessibility_settings_disable">Disable</string>
|
||||||
|
|
||||||
<string name="accessibility_service_name">Easy Launcher Actions Service</string>
|
<string name="accessibility_service_name">Easy Launcher Actions Service</string>
|
||||||
|
<string name="notification_badge_service_label">Easy Launcher - notification dots</string>
|
||||||
<string name="accessibility_service_desc">Please turn on accessibility service to use double tap to lock feature in Easy Launcher.\n\nThis permission is used only to turn off your screen. Our
|
<string name="accessibility_service_desc">Please turn on accessibility service to use double tap to lock feature in Easy Launcher.\n\nThis permission is used only to turn off your screen. Our
|
||||||
accessibility service does not collect or share any data.</string>
|
accessibility service does not collect or share any data.</string>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user