-
Notifications
You must be signed in to change notification settings - Fork 2
Fix/notification deduplication #3
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
Changes from all commits
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 | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,88 @@ | ||||||||||||||||||||||||||||
| name: Build & Release APK | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||
| push: | ||||||||||||||||||||||||||||
| tags: | ||||||||||||||||||||||||||||
| - 'v*.*.*' | ||||||||||||||||||||||||||||
| branches: | ||||||||||||||||||||||||||||
| - '**' | ||||||||||||||||||||||||||||
| pull_request: | ||||||||||||||||||||||||||||
| branches: | ||||||||||||||||||||||||||||
| - '**' | ||||||||||||||||||||||||||||
| workflow_dispatch: | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||
| build: | ||||||||||||||||||||||||||||
| name: Build Signed Release APK | ||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||
| - name: Checkout repository | ||||||||||||||||||||||||||||
| uses: actions/checkout@v4 | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| - name: Set up JDK 17 | ||||||||||||||||||||||||||||
| uses: actions/setup-java@v4 | ||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||
| java-version: '17' | ||||||||||||||||||||||||||||
| distribution: 'temurin' | ||||||||||||||||||||||||||||
| cache: gradle | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| - name: Setup Android SDK | ||||||||||||||||||||||||||||
| uses: android-actions/setup-android@v3 | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| - name: Grant execute permission for gradlew | ||||||||||||||||||||||||||||
| run: chmod +x gradlew | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| - name: Decode Keystore | ||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||
| echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > ${{ github.workspace }}/notif-forwarder-release.jks | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| - name: Build Release APK | ||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||
| ./gradlew assembleRelease \ | ||||||||||||||||||||||||||||
| -Pandroid.injected.signing.store.file=${{ github.workspace }}/notif-forwarder-release.jks \ | ||||||||||||||||||||||||||||
| -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} \ | ||||||||||||||||||||||||||||
| -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} \ | ||||||||||||||||||||||||||||
| -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }} | ||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+46
|
||||||||||||||||||||||||||||
| run: | | |
| ./gradlew assembleRelease \ | |
| -Pandroid.injected.signing.store.file=${{ github.workspace }}/notif-forwarder-release.jks \ | |
| -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} \ | |
| -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} \ | |
| -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }} | |
| env: | |
| ORG_GRADLE_PROJECT_signingStoreFile: ${{ github.workspace }}/notif-forwarder-release.jks | |
| ORG_GRADLE_PROJECT_signingStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} | |
| ORG_GRADLE_PROJECT_signingKeyAlias: ${{ secrets.KEY_ALIAS }} | |
| ORG_GRADLE_PROJECT_signingKeyPassword: ${{ secrets.KEY_PASSWORD }} | |
| run: | | |
| ./gradlew assembleRelease |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package com.itsazni.notificationforwarder.service | ||
|
|
||
| import android.app.Notification | ||
| import android.os.Build | ||
| import android.service.notification.NotificationListenerService | ||
| import android.service.notification.StatusBarNotification | ||
| import com.itsazni.notificationforwarder.data.NotificationRepository | ||
|
|
@@ -13,6 +14,15 @@ import kotlinx.coroutines.launch | |
| class AppNotificationListenerService : NotificationListenerService() { | ||
| private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) | ||
|
|
||
| private data class RecentEvent( | ||
| val contentHash: Int, | ||
| val postedAt: Long, | ||
| val seenAt: Long | ||
| ) | ||
|
|
||
| private val dedupLock = Any() | ||
| private val recentEvents = LinkedHashMap<String, RecentEvent>(MAX_RECENT_EVENTS, 0.75f, true) | ||
|
|
||
| override fun onNotificationPosted(sbn: StatusBarNotification?) { | ||
| super.onNotificationPosted(sbn) | ||
| val item = sbn ?: return | ||
|
|
@@ -24,6 +34,11 @@ class AppNotificationListenerService : NotificationListenerService() { | |
| val extras = notification.extras | ||
| val title = extras?.getCharSequence(Notification.EXTRA_TITLE)?.toString().orEmpty() | ||
| val text = extras?.getCharSequence(Notification.EXTRA_TEXT)?.toString().orEmpty() | ||
| val bigText = extras?.getCharSequence(Notification.EXTRA_BIG_TEXT)?.toString().orEmpty() | ||
|
|
||
| if (shouldSkip(item, notification, title, text, bigText)) { | ||
| return | ||
| } | ||
|
|
||
| serviceScope.launch { | ||
| val repository = NotificationRepository(applicationContext) | ||
|
|
@@ -46,4 +61,71 @@ class AppNotificationListenerService : NotificationListenerService() { | |
| pm.getApplicationLabel(applicationInfo).toString() | ||
| }.getOrDefault(pkg) | ||
| } | ||
|
|
||
| private fun shouldSkip( | ||
| sbn: StatusBarNotification, | ||
| notification: Notification, | ||
| title: String, | ||
| text: String, | ||
| bigText: String | ||
| ): Boolean { | ||
| val isGroupSummary = (notification.flags and Notification.FLAG_GROUP_SUMMARY) != 0 | ||
| if (isGroupSummary) { | ||
| return true | ||
| } | ||
|
|
||
| val stableKey = buildStableKey(sbn) | ||
| val contentHash = listOf(title, text, bigText).joinToString("\u001f").hashCode() | ||
| val now = System.currentTimeMillis() | ||
|
Comment on lines
+77
to
+79
|
||
|
|
||
| synchronized(dedupLock) { | ||
| val previous = recentEvents[stableKey] | ||
| if (previous != null) { | ||
| val sameContent = previous.contentHash == contentHash | ||
| val samePostTime = previous.postedAt == sbn.postTime | ||
| val burstUpdate = now - previous.seenAt <= DUPLICATE_WINDOW_MS | ||
| if (sameContent && (samePostTime || burstUpdate)) { | ||
|
Comment on lines
+79
to
+87
|
||
| return true | ||
| } | ||
| } | ||
|
|
||
| recentEvents[stableKey] = RecentEvent( | ||
| contentHash = contentHash, | ||
| postedAt = sbn.postTime, | ||
| seenAt = now | ||
| ) | ||
| trimRecentEvents() | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| private fun buildStableKey(sbn: StatusBarNotification): String { | ||
| val fallback = buildString { | ||
| append(sbn.packageName) | ||
| append('|') | ||
| append(sbn.id) | ||
| append('|') | ||
| append(sbn.tag ?: "") | ||
| append('|') | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { | ||
| append(sbn.user.hashCode()) | ||
| } else { | ||
| append("legacy-user") | ||
| } | ||
|
Comment on lines
+111
to
+115
|
||
| } | ||
| return sbn.key.ifBlank { fallback } | ||
| } | ||
|
|
||
| private fun trimRecentEvents() { | ||
| while (recentEvents.size > MAX_RECENT_EVENTS) { | ||
| val firstKey = recentEvents.entries.firstOrNull()?.key ?: return | ||
| recentEvents.remove(firstKey) | ||
| } | ||
| } | ||
|
|
||
| companion object { | ||
| private const val DUPLICATE_WINDOW_MS = 500L | ||
| private const val MAX_RECENT_EVENTS = 512 | ||
| } | ||
| } | ||
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.
This workflow runs on
pull_request, but the release build always injects signing credentials fromsecrets.*. On pull requests from forks, GitHub does not provide repository secrets, so these values are empty andassembleReleasetries to sign with an invalid/empty keystore, causing the CI job to fail for external contributors. Add a condition to skip signed-release steps (or build unsigned) when secrets are not available.Useful? React with 👍 / 👎.