-
-
Notifications
You must be signed in to change notification settings - Fork 956
Progress bar customization #6527
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bfb5e9a
90f98e9
58d3bd8
468a3b9
d66ece6
793f63e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,8 @@ import android.content.Intent | |
| import android.content.pm.PackageManager | ||
| import android.graphics.Bitmap | ||
| import android.graphics.BitmapFactory | ||
| import android.graphics.PorterDuff | ||
| import android.graphics.PorterDuffColorFilter | ||
| import android.graphics.drawable.Icon | ||
| import android.media.AudioManager | ||
| import android.media.MediaMetadataRetriever | ||
|
|
@@ -40,9 +42,17 @@ import androidx.core.app.RemoteInput | |
| import androidx.core.content.ContextCompat | ||
| import androidx.core.content.FileProvider | ||
| import androidx.core.content.getSystemService | ||
| import androidx.core.content.withStyledAttributes | ||
| import androidx.core.graphics.scale | ||
| import androidx.core.net.toUri | ||
| import androidx.core.text.isDigitsOnly | ||
| import com.google.android.material.color.DynamicColors | ||
| import com.mikepenz.iconics.IconicsDrawable | ||
| import com.mikepenz.iconics.utils.backgroundColorRes | ||
| import com.mikepenz.iconics.utils.paddingDp | ||
| import com.mikepenz.iconics.utils.roundedCornersDp | ||
| import com.mikepenz.iconics.utils.sizeDp | ||
| import com.mikepenz.iconics.utils.toAndroidIconCompat | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import io.homeassistant.companion.android.R | ||
| import io.homeassistant.companion.android.authenticator.Authenticator | ||
|
|
@@ -63,6 +73,7 @@ import io.homeassistant.companion.android.common.notifications.handleDeleteInten | |
| import io.homeassistant.companion.android.common.notifications.handleSmallIcon | ||
| import io.homeassistant.companion.android.common.notifications.handleText | ||
| import io.homeassistant.companion.android.common.notifications.parseColor | ||
| import io.homeassistant.companion.android.common.notifications.parseFlattenedList | ||
| import io.homeassistant.companion.android.common.notifications.parseVibrationPattern | ||
| import io.homeassistant.companion.android.common.notifications.prepareText | ||
| import io.homeassistant.companion.android.common.util.cancelGroupIfNeeded | ||
|
|
@@ -146,6 +157,18 @@ class MessagingManager @Inject constructor( | |
| const val PROGRESS = "progress" | ||
| const val PROGRESS_MAX = "progress_max" | ||
| const val PROGRESS_INDETERMINATE = "progress_indeterminate" | ||
| const val PROGRESS_SEGMENTS = "progress_segments" | ||
| const val PROGRESS_SEGMENTS_LENGTH = "length" | ||
| const val PROGRESS_SEGMENTS_COLOR = "color" | ||
| const val PROGRESS_POINTS = "progress_points" | ||
| const val PROGRESS_POINTS_POSITION = "position" | ||
| const val PROGRESS_POINTS_COLOR = "color" | ||
| const val PROGRESS_TRACKER_ICON = "progress_tracker_icon" | ||
| const val PROGRESS_TRACKER_COLOR = "progress_tracker_color" | ||
| const val PROGRESS_START_ICON = "progress_start_icon" | ||
| const val PROGRESS_START_COLOR = "progress_start_color" | ||
| const val PROGRESS_END_ICON = "progress_end_icon" | ||
| const val PROGRESS_END_COLOR = "progress_end_color" | ||
| const val LIVE_UPDATE = "live_update" | ||
| const val CRITICAL_TEXT = "critical_text" | ||
| const val CAR_UI = "car_ui" | ||
|
|
@@ -1075,6 +1098,8 @@ class MessagingManager @Inject constructor( | |
|
|
||
| handleProgress(notificationBuilder, data) | ||
|
|
||
| handleProgressStyle(notificationBuilder, data) | ||
|
|
||
| handleLive(notificationBuilder, data) | ||
|
|
||
| val useCarNotification = handleCarUiVisible(context, notificationBuilder, data) | ||
|
|
@@ -1151,6 +1176,132 @@ class MessagingManager @Inject constructor( | |
| } | ||
| } | ||
|
|
||
| private fun handleProgressStyle(builder: NotificationCompat.Builder, data: Map<String, String>) { | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { | ||
| val progressStyle = NotificationCompat.ProgressStyle().setStyledByProgress(true) | ||
| .setProgress(data[PROGRESS]?.toIntOrNull() ?: -1) | ||
|
|
||
| val dynamicColorContext = DynamicColors.wrapContextIfAvailable(context) | ||
| var accentColor = commonR.color.colorAccent | ||
| dynamicColorContext.withStyledAttributes(null, intArrayOf(android.R.attr.colorAccent)) { | ||
| accentColor = getResourceId(0, accentColor) | ||
| } | ||
|
inukiwi marked this conversation as resolved.
|
||
|
|
||
| var useProgressStyle = false | ||
| if (applyProgressSegments(progressStyle, accentColor, data)) useProgressStyle = true | ||
| if (applyProgressPoints(progressStyle, accentColor, data)) useProgressStyle = true | ||
| if (applyProgressStartIcon(progressStyle, accentColor, data)) useProgressStyle = true | ||
| if (applyProgressEndIcon(progressStyle, accentColor, data)) useProgressStyle = true | ||
| if (applyProgressTrackerIcon(progressStyle, accentColor, data)) useProgressStyle = true | ||
| if (useProgressStyle) { | ||
| builder.setStyle(progressStyle) | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
||
| private fun applyProgressSegments(style: NotificationCompat.ProgressStyle, accentColor: Int, data: Map<String, String>): Boolean { | ||
| val segmentsData = data[PROGRESS_SEGMENTS] ?: "" | ||
| if (segmentsData.isNotBlank()) | ||
| { | ||
| val segments = parseFlattenedList(segmentsData) | ||
| for (segment in segments) { | ||
| val length = segment[PROGRESS_SEGMENTS_LENGTH]?.toIntOrNull() ?: 0 | ||
| val color = segment[PROGRESS_SEGMENTS_COLOR] | ||
| val progressSegment = NotificationCompat.ProgressStyle.Segment(length) | ||
| if (color != null) { | ||
| progressSegment.setColor(parseColor(context, color, accentColor)) | ||
| } | ||
| style.addProgressSegment(progressSegment) | ||
| } | ||
| if (segments.isNotEmpty()) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| private fun applyProgressPoints(style: NotificationCompat.ProgressStyle, accentColor: Int, data: Map<String, String>): Boolean { | ||
| val pointsData = data[PROGRESS_POINTS] ?: "" | ||
| if (pointsData.isNotBlank()) { | ||
| val points = parseFlattenedList(pointsData) | ||
| for (point in points) { | ||
| val position = point[PROGRESS_POINTS_POSITION]?.toIntOrNull() ?: 0 | ||
| val color = point[PROGRESS_POINTS_COLOR] | ||
| val progressPoint = NotificationCompat.ProgressStyle.Point(position) | ||
| if (color != null) { | ||
| progressPoint.setColor(parseColor(context, color, accentColor)) | ||
| } | ||
| style.addProgressPoint(progressPoint) | ||
| } | ||
| if (points.isNotEmpty()) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| private fun applyProgressTrackerIcon(style: NotificationCompat.ProgressStyle, accentColor: Int, data: Map<String, String>): Boolean { | ||
| val progressTrackerIcon = data[PROGRESS_TRACKER_ICON] ?: "" | ||
| val progressTrackerColor = | ||
| parseColor(context, data[PROGRESS_TRACKER_COLOR] ?: "", commonR.color.colorPrimary) | ||
| if (progressTrackerIcon.startsWith("mdi:") && progressTrackerIcon.substringAfter("mdi:").isNotBlank()) { | ||
| val iconName = progressTrackerIcon.split(":")[1] | ||
|
Comment on lines
+1248
to
+1249
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to be on the safe side we might want to verify the size while doing the split.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| val iconDrawable = IconicsDrawable(context, "cmd-$iconName").apply { | ||
| sizeDp = 20 | ||
| colorFilter = PorterDuffColorFilter(progressTrackerColor, PorterDuff.Mode.SRC_IN) | ||
| backgroundColorRes = accentColor | ||
| roundedCornersDp = 10 | ||
| paddingDp = 4 | ||
| } | ||
|
inukiwi marked this conversation as resolved.
Comment on lines
+1251
to
+1256
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would like some docs about the choice of these magic numbers. |
||
| if (iconDrawable.icon != null) { | ||
| style.setProgressTrackerIcon( | ||
| iconDrawable.toAndroidIconCompat(), | ||
| ) | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| private fun applyProgressStartIcon(style: NotificationCompat.ProgressStyle, accentColor: Int, data: Map<String, String>): Boolean { | ||
| val progressStartIcon = data[PROGRESS_START_ICON] ?: "" | ||
| val progressStartColor = parseColor(context, data[PROGRESS_START_COLOR] ?: "", accentColor) | ||
| if (progressStartIcon.startsWith("mdi:") && progressStartIcon.substringAfter("mdi:").isNotBlank()) { | ||
| val iconName = progressStartIcon.split(":")[1] | ||
|
Comment on lines
+1270
to
+1271
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same and could actually be wrap into a small helper |
||
| val iconDrawable = IconicsDrawable(context, "cmd-$iconName").apply { | ||
| sizeDp = 20 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here or even a const |
||
| colorFilter = PorterDuffColorFilter(progressStartColor, PorterDuff.Mode.SRC_IN) | ||
| } | ||
| if (iconDrawable.icon != null) { | ||
| style.setProgressStartIcon( | ||
| iconDrawable.toAndroidIconCompat(), | ||
| ) | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| private fun applyProgressEndIcon(style: NotificationCompat.ProgressStyle, accentColor: Int, data: Map<String, String>): Boolean { | ||
| val progressEndIcon = data[PROGRESS_END_ICON] ?: "" | ||
| val progressEndColor = parseColor(context, data[PROGRESS_END_COLOR] ?: "", accentColor) | ||
| if (progressEndIcon.startsWith("mdi:") && progressEndIcon.substringAfter("mdi:").isNotBlank()) { | ||
| val iconName = progressEndIcon.split(":")[1] | ||
|
Comment on lines
+1289
to
+1290
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same |
||
| val iconDrawable = IconicsDrawable(context, "cmd-$iconName").apply { | ||
| sizeDp = 20 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here or even a const |
||
| colorFilter = PorterDuffColorFilter(progressEndColor, PorterDuff.Mode.SRC_IN) | ||
| } | ||
| if (iconDrawable.icon != null) { | ||
| style.setProgressEndIcon( | ||
| iconDrawable.toAndroidIconCompat(), | ||
| ) | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
|
|
||
| private fun handleLive(builder: NotificationCompat.Builder, data: Map<String, String>) { | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { | ||
| val liveUpdate = data[LIVE_UPDATE]?.toBoolean() ?: false | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -290,3 +290,24 @@ fun handleDeleteIntent( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| builder.setDeleteIntent(deletePendingIntent) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun parseFlattenedList(flattenedList: String): List<Map<String, String>> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is quite specific no? It is a public function it's quite bad to expose it to the rest of the app, what would you think if you were proposed this function by the IDE without knowing the internal and where it can be used? I would also this to be documented with examples. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val list: List<Map<String, String>> = flattenedList | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .trim() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .removeSurrounding("[", "]") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .split("}, {") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map { segment -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| segment.trim().removePrefix("{").removeSuffix("}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .split(",") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .associate { pair -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val (key, value) = pair.split("=") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key.trim() to value.trim() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return list | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e: Exception) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error catching is too broad can you identify what can throw and only catch this? |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Timber.tag(NotificationData.TAG).e(e, "Unable to parse flattened list") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return emptyList() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+295
to
+311
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | |
| val list: List<Map<String, String>> = flattenedList | |
| .trim() | |
| .removeSurrounding("[", "]") | |
| .split("}, {") | |
| .map { segment -> | |
| segment.trim().removePrefix("{").removeSuffix("}") | |
| .split(",") | |
| .associate { pair -> | |
| val (key, value) = pair.split("=") | |
| key.trim() to value.trim() | |
| } | |
| } | |
| return list | |
| } catch (e: Exception) { | |
| Timber.tag(NotificationData.TAG).e(e, "Unable to parse flattened list") | |
| return emptyList() | |
| val content = flattenedList | |
| .trim() | |
| .removeSurrounding("[", "]") | |
| .trim() | |
| if (content.isBlank()) { | |
| // Treat an empty or whitespace-only list as a valid empty result | |
| return emptyList() | |
| } | |
| return try { | |
| val segmentSeparator = Regex("\\}\\s*,\\s*\\{") | |
| content | |
| .split(segmentSeparator) | |
| .map { segment -> | |
| val normalizedSegment = segment | |
| .trim() | |
| .removePrefix("{") | |
| .removeSuffix("}") | |
| .trim() | |
| if (normalizedSegment.isBlank()) { | |
| emptyMap() | |
| } else { | |
| normalizedSegment | |
| .split(Regex("\\s*,\\s*")) | |
| .filter { it.isNotBlank() } | |
| .associate { pair -> | |
| val parts = pair.split("=", limit = 2) | |
| val key = parts.getOrNull(0)?.trim().orEmpty() | |
| val value = parts.getOrNull(1)?.trim().orEmpty() | |
| key to value | |
| } | |
| } | |
| } | |
| } catch (e: Exception) { | |
| Timber.tag(NotificationData.TAG).e(e, "Unable to parse flattened list") | |
| emptyList() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I opted for a minimal method here that only handles valid lists with a depth of 1 and defaults to an empty list.
If we want this method to be used in the future for other purposes I can also rewrite it into a recursive method that handles all nested objects/lists
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you make it public, you need to write down the limitations.
Uh oh!
There was an error while loading. Please reload this page.