Skip to content

Fix/notification deduplication#3

Merged
ItsAzni merged 2 commits into
masterfrom
fix/notification-deduplication
Apr 20, 2026
Merged

Fix/notification deduplication#3
ItsAzni merged 2 commits into
masterfrom
fix/notification-deduplication

Conversation

@ItsAzni
Copy link
Copy Markdown
Owner

@ItsAzni ItsAzni commented Apr 20, 2026

No description provided.

Copilot AI review requested due to automatic review settings April 20, 2026 06:53
@ItsAzni ItsAzni linked an issue Apr 20, 2026 that may be closed by this pull request
@ItsAzni ItsAzni self-assigned this Apr 20, 2026
@ItsAzni ItsAzni added the bug Something isn't working label Apr 20, 2026
@ItsAzni ItsAzni merged commit f917c1f into master Apr 20, 2026
4 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds in-service notification deduplication to reduce repeated forwarding, and introduces a GitHub Actions workflow to build (and on tags, release) APK artifacts.

Changes:

  • Add a small in-memory LRU-based dedup cache in AppNotificationListenerService to skip group summaries and suppress rapid duplicates.
  • Expand notification content extraction to include EXTRA_BIG_TEXT for better dedup matching.
  • Add a CI workflow to build a signed release APK, upload it as an artifact, and create a GitHub Release on version tags.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
app/src/main/java/com/itsazni/notificationforwarder/service/AppNotificationListenerService.kt Adds dedup logic keyed by notification identity + content signature to skip duplicates/group summaries.
.github/workflows/build.yml Adds CI build-and-release pipeline for APK artifacts and tag-based GitHub Releases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +36 to +46
- 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 }}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keystore decode step runs for every push/PR, but secrets are not available for forked PRs (and may be intentionally unset in some environments), which will cause this workflow to fail before any build happens. Consider gating keystore decode/signing to tag builds (or when all required secrets are present), and running an unsigned/debug build for PRs/branch pushes instead.

Copilot uses AI. Check for mistakes.
Comment on lines +41 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 }}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow uses the "android.injected.signing.*" properties to sign the release. That approach is intended for IDE/injected builds and can be brittle in CI (and may break with future AGP updates). Prefer configuring signingConfigs in Gradle and selecting the signing config via environment variables/Gradle properties, or use a dedicated Gradle task for CI signing.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +111 to +115
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
append(sbn.user.hashCode())
} else {
append("legacy-user")
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build.VERSION.SDK_INT >= JELLY_BEAN_MR1 is always true in this app (minSdk is 26), so the else branch is dead code. Consider simplifying this to always include sbn.user (or removing the version check entirely) to reduce noise in the key calculation.

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +87
val now = System.currentTimeMillis()

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)) {
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dedup window uses System.currentTimeMillis() for seenAt and then compares deltas. Wall-clock time can jump backwards/forwards (manual change, NTP), which can break the burstUpdate calculation and lead to missed or extra forwarding. Consider using a monotonic clock (e.g., SystemClock.elapsedRealtime()) for seenAt/window comparisons while keeping sbn.postTime as-is.

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +79
val stableKey = buildStableKey(sbn)
val contentHash = listOf(title, text, bigText).joinToString("\u001f").hashCode()
val now = System.currentTimeMillis()
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contentHash is derived from String.hashCode() over a concatenated string, which can collide and incorrectly suppress distinct notifications. If the goal is correctness over micro-optimizations, consider using a stronger/fewer-collisions content signature (e.g., a 64-bit hash or MessageDigest) and avoid the intermediate List/joined String allocation.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0b75ef121

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +40 to +44
- 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 }} \
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate signed release build when secrets are unavailable

This workflow runs on pull_request, but the release build always injects signing credentials from secrets.*. On pull requests from forks, GitHub does not provide repository secrets, so these values are empty and assembleRelease tries 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 👍 / 👎.

@ItsAzni ItsAzni linked an issue Apr 26, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] config/template/variable options for webhook [BUG] duplicate notifications were sent

2 participants