Only fetch and display a given notification once (#3626)
When fetching: - Maintain a marker with the position of the newest fetched notification - Use the marker to determine which notifications to fetch - Fetch notifications with min_id to ensure that none are lost - Update the marker as necessary - Perform a one-time immediate fetch of notifications on startup When creating notifications: - Identify each notification with tag=${MastodonNotificationId}, id=${account.id} - Remove activeNotifications field, it's no longer necessary - Use the tag/id tuple to reliably identify existing notifications and avoid creating duplicates - Cancelling notifications for an account must iterate over all the notifications, and individually remove the notifications that exist for that account. - Limit notifications to a maximum of 40 (excluding summary notifications) - Remove notifications (oldest first) to get under this limit - Rate limit notification creation to 1 per second, so the OS won't drop them Adjust the summary notification: - Ensure the summary notification and the child notifications have the same group key - Dismiss the summary notification if there is only one child notification NotificationClearBroadcastReceiver is no longer needed, so remove it, and the need for deletePendingIntent. Fixes #3625, #3539
This commit is contained in:
parent
74a00c0591
commit
81b15e72f3
11 changed files with 1327 additions and 197 deletions
995
app/schemas/com.keylesspalace.tusky.db.AppDatabase/50.json
Normal file
995
app/schemas/com.keylesspalace.tusky.db.AppDatabase/50.json
Normal file
|
@ -0,0 +1,995 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 50,
|
||||||
|
"identityHash": "4eaf69e915d4a15f021547b725101acd",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "DraftEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentWarning",
|
||||||
|
"columnName": "contentWarning",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "failedToSend",
|
||||||
|
"columnName": "failedToSend",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "failedToSendNew",
|
||||||
|
"columnName": "failedToSendNew",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "scheduledAt",
|
||||||
|
"columnName": "scheduledAt",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "language",
|
||||||
|
"columnName": "language",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "statusId",
|
||||||
|
"columnName": "statusId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "AccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL, `lastVisibleHomeTimelineStatusId` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domain",
|
||||||
|
"columnName": "domain",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessToken",
|
||||||
|
"columnName": "accessToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "clientId",
|
||||||
|
"columnName": "clientId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "clientSecret",
|
||||||
|
"columnName": "clientSecret",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isActive",
|
||||||
|
"columnName": "isActive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "profilePictureUrl",
|
||||||
|
"columnName": "profilePictureUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsEnabled",
|
||||||
|
"columnName": "notificationsEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsMentioned",
|
||||||
|
"columnName": "notificationsMentioned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowed",
|
||||||
|
"columnName": "notificationsFollowed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowRequested",
|
||||||
|
"columnName": "notificationsFollowRequested",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsReblogged",
|
||||||
|
"columnName": "notificationsReblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFavorited",
|
||||||
|
"columnName": "notificationsFavorited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsPolls",
|
||||||
|
"columnName": "notificationsPolls",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSubscriptions",
|
||||||
|
"columnName": "notificationsSubscriptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSignUps",
|
||||||
|
"columnName": "notificationsSignUps",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsUpdates",
|
||||||
|
"columnName": "notificationsUpdates",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsReports",
|
||||||
|
"columnName": "notificationsReports",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostLanguage",
|
||||||
|
"columnName": "defaultPostLanguage",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "oauthScopes",
|
||||||
|
"columnName": "oauthScopes",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unifiedPushUrl",
|
||||||
|
"columnName": "unifiedPushUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPubKey",
|
||||||
|
"columnName": "pushPubKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushPrivKey",
|
||||||
|
"columnName": "pushPrivKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushAuth",
|
||||||
|
"columnName": "pushAuth",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pushServerKey",
|
||||||
|
"columnName": "pushServerKey",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastVisibleHomeTimelineStatusId",
|
||||||
|
"columnName": "lastVisibleHomeTimelineStatusId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "InstanceEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "instance",
|
||||||
|
"columnName": "instance",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojiList",
|
||||||
|
"columnName": "emojiList",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maximumTootCharacters",
|
||||||
|
"columnName": "maximumTootCharacters",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptions",
|
||||||
|
"columnName": "maxPollOptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptionLength",
|
||||||
|
"columnName": "maxPollOptionLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "minPollDuration",
|
||||||
|
"columnName": "minPollDuration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollDuration",
|
||||||
|
"columnName": "maxPollDuration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "charactersReservedPerUrl",
|
||||||
|
"columnName": "charactersReservedPerUrl",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoSizeLimit",
|
||||||
|
"columnName": "videoSizeLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageSizeLimit",
|
||||||
|
"columnName": "imageSizeLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageMatrixLimit",
|
||||||
|
"columnName": "imageMatrixLimit",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxMediaAttachments",
|
||||||
|
"columnName": "maxMediaAttachments",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFields",
|
||||||
|
"columnName": "maxFields",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFieldNameLength",
|
||||||
|
"columnName": "maxFieldNameLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxFieldValueLength",
|
||||||
|
"columnName": "maxFieldValueLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"instance"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineStatusEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `filtered` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorServerId",
|
||||||
|
"columnName": "authorServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToAccountId",
|
||||||
|
"columnName": "inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "editedAt",
|
||||||
|
"columnName": "editedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogsCount",
|
||||||
|
"columnName": "reblogsCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favouritesCount",
|
||||||
|
"columnName": "favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repliesCount",
|
||||||
|
"columnName": "repliesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muted",
|
||||||
|
"columnName": "muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "expanded",
|
||||||
|
"columnName": "expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentCollapsed",
|
||||||
|
"columnName": "contentCollapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentShowing",
|
||||||
|
"columnName": "contentShowing",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pinned",
|
||||||
|
"columnName": "pinned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "card",
|
||||||
|
"columnName": "card",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "language",
|
||||||
|
"columnName": "language",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "filtered",
|
||||||
|
"columnName": "filtered",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "TimelineAccountEntity",
|
||||||
|
"onDelete": "NO ACTION",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineAccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localUsername",
|
||||||
|
"columnName": "localUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatar",
|
||||||
|
"columnName": "avatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bot",
|
||||||
|
"columnName": "bot",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ConversationEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accounts",
|
||||||
|
"columnName": "accounts",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.id",
|
||||||
|
"columnName": "s_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.url",
|
||||||
|
"columnName": "s_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToId",
|
||||||
|
"columnName": "s_inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToAccountId",
|
||||||
|
"columnName": "s_inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.account",
|
||||||
|
"columnName": "s_account",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.content",
|
||||||
|
"columnName": "s_content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.createdAt",
|
||||||
|
"columnName": "s_createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.editedAt",
|
||||||
|
"columnName": "s_editedAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.emojis",
|
||||||
|
"columnName": "s_emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favouritesCount",
|
||||||
|
"columnName": "s_favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.repliesCount",
|
||||||
|
"columnName": "s_repliesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favourited",
|
||||||
|
"columnName": "s_favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.bookmarked",
|
||||||
|
"columnName": "s_bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.sensitive",
|
||||||
|
"columnName": "s_sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.spoilerText",
|
||||||
|
"columnName": "s_spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.attachments",
|
||||||
|
"columnName": "s_attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.mentions",
|
||||||
|
"columnName": "s_mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.tags",
|
||||||
|
"columnName": "s_tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.showingHiddenContent",
|
||||||
|
"columnName": "s_showingHiddenContent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.expanded",
|
||||||
|
"columnName": "s_expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsed",
|
||||||
|
"columnName": "s_collapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.muted",
|
||||||
|
"columnName": "s_muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.poll",
|
||||||
|
"columnName": "s_poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.language",
|
||||||
|
"columnName": "s_language",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"accountId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4eaf69e915d4a15f021547b725101acd')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -155,8 +155,6 @@
|
||||||
<activity android:name="com.keylesspalace.tusky.components.filters.EditFilterActivity"
|
<activity android:name="com.keylesspalace.tusky.components.filters.EditFilterActivity"
|
||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver"
|
|
||||||
android:exported="false" />
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".receiver.SendStatusBroadcastReceiver"
|
android:name=".receiver.SendStatusBroadcastReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
|
|
@ -1,15 +1,27 @@
|
||||||
package com.keylesspalace.tusky.components.notifications
|
package com.keylesspalace.tusky.components.notifications
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper.filterNotification
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.entity.Marker
|
import com.keylesspalace.tusky.entity.Marker
|
||||||
import com.keylesspalace.tusky.entity.Notification
|
import com.keylesspalace.tusky.entity.Notification
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.isLessThan
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Mastodon notifications and show Android notifications, with summaries, for them.
|
||||||
|
*
|
||||||
|
* Should only be called by a worker thread.
|
||||||
|
*
|
||||||
|
* @see NotificationWorker
|
||||||
|
* @see <a href="https://developer.android.com/guide/background/persistent/threading/worker">Background worker</a>
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
class NotificationFetcher @Inject constructor(
|
class NotificationFetcher @Inject constructor(
|
||||||
private val mastodonApi: MastodonApi,
|
private val mastodonApi: MastodonApi,
|
||||||
private val accountManager: AccountManager,
|
private val accountManager: AccountManager,
|
||||||
|
@ -19,46 +31,111 @@ class NotificationFetcher @Inject constructor(
|
||||||
for (account in accountManager.getAllAccountsOrderedByActive()) {
|
for (account in accountManager.getAllAccountsOrderedByActive()) {
|
||||||
if (account.notificationsEnabled) {
|
if (account.notificationsEnabled) {
|
||||||
try {
|
try {
|
||||||
val notifications = fetchNotifications(account)
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notifications.forEachIndexed { index, notification ->
|
|
||||||
NotificationHelper.make(context, notification, account, index == 0)
|
// Create sorted list of new notifications
|
||||||
|
val notifications = fetchNewNotifications(account)
|
||||||
|
.filter { filterNotification(notificationManager, account, it) }
|
||||||
|
.sortedWith(compareBy({ it.id.length }, { it.id })) // oldest notifications first
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
|
// There's a maximum limit on the number of notifications an Android app
|
||||||
|
// can display. If the total number of notifications (current notifications,
|
||||||
|
// plus new ones) exceeds this then some newer notifications will be dropped.
|
||||||
|
//
|
||||||
|
// Err on the side of removing *older* notifications to make room for newer
|
||||||
|
// notifications.
|
||||||
|
val currentAndroidNotifications = notificationManager.activeNotifications
|
||||||
|
.sortedWith(compareBy({ it.tag.length }, { it.tag })) // oldest notifications first
|
||||||
|
|
||||||
|
// Check to see if any notifications need to be removed
|
||||||
|
val toRemove = currentAndroidNotifications.size + notifications.size - MAX_NOTIFICATIONS
|
||||||
|
if (toRemove > 0) {
|
||||||
|
// Prefer to cancel old notifications first
|
||||||
|
currentAndroidNotifications.subList(0, min(toRemove, currentAndroidNotifications.size))
|
||||||
|
.forEach { notificationManager.cancel(it.tag, it.id) }
|
||||||
|
|
||||||
|
// Still got notifications to remove? Trim the list of new notifications,
|
||||||
|
// starting with the oldest.
|
||||||
|
while (notifications.size > MAX_NOTIFICATIONS) {
|
||||||
|
notifications.removeAt(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make and send the new notifications
|
||||||
|
// TODO: Use the batch notification API available in NotificationManagerCompat
|
||||||
|
// 1.11 and up (https://developer.android.com/jetpack/androidx/releases/core#1.11.0-alpha01)
|
||||||
|
// when it is released.
|
||||||
|
notifications.forEachIndexed { index, notification ->
|
||||||
|
val androidNotification = NotificationHelper.make(
|
||||||
|
context,
|
||||||
|
notificationManager,
|
||||||
|
notification,
|
||||||
|
account,
|
||||||
|
index == 0
|
||||||
|
)
|
||||||
|
notificationManager.notify(notification.id, account.id.toInt(), androidNotification)
|
||||||
|
// Android will rate limit / drop notifications if they're posted too
|
||||||
|
// quickly. There is no indication to the user that this happened.
|
||||||
|
// See https://github.com/tuskyapp/Tusky/pull/3626#discussion_r1192963664
|
||||||
|
Thread.sleep(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationHelper.updateSummaryNotifications(
|
||||||
|
context,
|
||||||
|
notificationManager,
|
||||||
|
account
|
||||||
|
)
|
||||||
|
|
||||||
accountManager.saveAccount(account)
|
accountManager.saveAccount(account)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Error while fetching notifications", e)
|
Log.e(TAG, "Error while fetching notifications", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchNotifications(account: AccountEntity): MutableList<Notification> {
|
/**
|
||||||
|
* Fetch new Mastodon Notifications and update the marker position.
|
||||||
|
*
|
||||||
|
* Here, "new" means "notifications with IDs newer than notifications the user has already
|
||||||
|
* seen."
|
||||||
|
*
|
||||||
|
* The "water mark" for Mastodon Notification IDs are stored in two places.
|
||||||
|
*
|
||||||
|
* - acccount.lastNotificationId -- the ID of the top-most notification when the user last
|
||||||
|
* left the Notifications tab.
|
||||||
|
* - The Mastodon "marker" API -- the ID of the most recent notification fetched here.
|
||||||
|
*
|
||||||
|
* The user may have refreshed the "Notifications" tab and seen notifications newer than the
|
||||||
|
* ones that were last fetched here. So `lastNotificationId` takes precedence if it is greater
|
||||||
|
* than the marker.
|
||||||
|
*/
|
||||||
|
private fun fetchNewNotifications(account: AccountEntity): List<Notification> {
|
||||||
val authHeader = String.format("Bearer %s", account.accessToken)
|
val authHeader = String.format("Bearer %s", account.accessToken)
|
||||||
// We fetch marker to not load/show notifications which user has already seen
|
|
||||||
val marker = fetchMarker(authHeader, account)
|
val minId = when (val marker = fetchMarker(authHeader, account)) {
|
||||||
if (marker != null && account.lastNotificationId.isLessThan(marker.lastReadId)) {
|
null -> account.lastNotificationId.takeIf { it != "0" }
|
||||||
account.lastNotificationId = marker.lastReadId
|
else -> if (marker.lastReadId > account.lastNotificationId) marker.lastReadId else account.lastNotificationId
|
||||||
}
|
}
|
||||||
Log.d(TAG, "getting Notifications for " + account.fullName)
|
|
||||||
|
Log.d(TAG, "getting Notifications for ${account.fullName}, min_id: $minId")
|
||||||
|
|
||||||
val notifications = mastodonApi.notificationsWithAuth(
|
val notifications = mastodonApi.notificationsWithAuth(
|
||||||
authHeader,
|
authHeader,
|
||||||
account.domain,
|
account.domain,
|
||||||
account.lastNotificationId
|
minId
|
||||||
).blockingGet()
|
).blockingGet()
|
||||||
|
|
||||||
val newId = account.lastNotificationId
|
// Notifications are returned in order, most recent first. Save the newest notification ID
|
||||||
var newestId = ""
|
// in the marker.
|
||||||
val result = mutableListOf<Notification>()
|
notifications.firstOrNull()?.let {
|
||||||
for (notification in notifications.reversed()) {
|
val newMarkerId = notifications.first().id
|
||||||
val currentId = notification.id
|
Log.d(TAG, "updating notification marker to: $newMarkerId")
|
||||||
if (newestId.isLessThan(currentId)) {
|
mastodonApi.updateMarkersWithAuth(authHeader, notificationsLastReadId = newMarkerId)
|
||||||
newestId = currentId
|
|
||||||
account.lastNotificationId = currentId
|
|
||||||
}
|
|
||||||
if (newId.isLessThan(currentId)) {
|
|
||||||
result.add(notification)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
|
return notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchMarker(authHeader: String, account: AccountEntity): Marker? {
|
private fun fetchMarker(authHeader: String, account: AccountEntity): Marker? {
|
||||||
|
@ -78,6 +155,13 @@ class NotificationFetcher @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "NotificationFetcher"
|
private const val TAG = "NotificationFetcher"
|
||||||
|
|
||||||
|
// There's a system limit on the maximum number of notifications an app
|
||||||
|
// can show, NotificationManagerService.MAX_PACKAGE_NOTIFICATIONS. Unfortunately
|
||||||
|
// that's not available to client code or via the NotificationManager API.
|
||||||
|
// The current value in the Android source code is 50, set 40 here to both
|
||||||
|
// be conservative, and allow some headroom for summary notifications.
|
||||||
|
private const val MAX_NOTIFICATIONS = 40
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.notifications;
|
package com.keylesspalace.tusky.components.notifications;
|
||||||
|
|
||||||
|
import static com.keylesspalace.tusky.BuildConfig.APPLICATION_ID;
|
||||||
import static com.keylesspalace.tusky.util.StatusParsingHelper.parseAsMastodonHtml;
|
import static com.keylesspalace.tusky.util.StatusParsingHelper.parseAsMastodonHtml;
|
||||||
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
|
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
|
||||||
|
|
||||||
|
@ -28,18 +29,21 @@ import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
import android.service.notification.StatusBarNotification;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
import androidx.core.app.RemoteInput;
|
import androidx.core.app.RemoteInput;
|
||||||
import androidx.core.app.TaskStackBuilder;
|
import androidx.core.app.TaskStackBuilder;
|
||||||
import androidx.work.Constraints;
|
import androidx.work.Constraints;
|
||||||
import androidx.work.NetworkType;
|
import androidx.work.NetworkType;
|
||||||
|
import androidx.work.OneTimeWorkRequest;
|
||||||
|
import androidx.work.OutOfQuotaPolicy;
|
||||||
import androidx.work.PeriodicWorkRequest;
|
import androidx.work.PeriodicWorkRequest;
|
||||||
import androidx.work.WorkManager;
|
import androidx.work.WorkManager;
|
||||||
import androidx.work.WorkRequest;
|
import androidx.work.WorkRequest;
|
||||||
|
@ -56,25 +60,20 @@ import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.entity.Poll;
|
import com.keylesspalace.tusky.entity.Poll;
|
||||||
import com.keylesspalace.tusky.entity.PollOption;
|
import com.keylesspalace.tusky.entity.PollOption;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
|
|
||||||
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver;
|
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver;
|
||||||
import com.keylesspalace.tusky.util.StringUtils;
|
import com.keylesspalace.tusky.util.StringUtils;
|
||||||
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
|
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.core.Single;
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
|
||||||
|
|
||||||
public class NotificationHelper {
|
public class NotificationHelper {
|
||||||
|
|
||||||
private static int notificationId = 0;
|
private static int notificationId = 0;
|
||||||
|
@ -84,7 +83,7 @@ public class NotificationHelper {
|
||||||
*/
|
*/
|
||||||
public static final String ACCOUNT_ID = "account_id";
|
public static final String ACCOUNT_ID = "account_id";
|
||||||
|
|
||||||
public static final String TYPE = "type";
|
public static final String TYPE = APPLICATION_ID + ".notification.type";
|
||||||
|
|
||||||
private static final String TAG = "NotificationHelper";
|
private static final String TAG = "NotificationHelper";
|
||||||
|
|
||||||
|
@ -127,51 +126,53 @@ public class NotificationHelper {
|
||||||
*/
|
*/
|
||||||
private static final String NOTIFICATION_PULL_TAG = "pullNotifications";
|
private static final String NOTIFICATION_PULL_TAG = "pullNotifications";
|
||||||
|
|
||||||
|
/** Tag for the summary notification */
|
||||||
|
private static final String GROUP_SUMMARY_TAG = APPLICATION_ID + ".notification.group_summary";
|
||||||
|
|
||||||
|
/** The name of the account that caused the notification, for use in a summary */
|
||||||
|
private static final String EXTRA_ACCOUNT_NAME = APPLICATION_ID + ".notification.extra.account_name";
|
||||||
|
|
||||||
|
/** The notification's type (string representation of a Notification.Type) */
|
||||||
|
private static final String EXTRA_NOTIFICATION_TYPE = APPLICATION_ID + ".notification.extra.notification_type";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a given Mastodon notification and either creates a new Android notification or updates
|
* Takes a given Mastodon notification and creates a new Android notification or updates the
|
||||||
* the state of the existing notification to reflect the new interaction.
|
* existing Android notification.
|
||||||
|
* <p>
|
||||||
|
* The Android notification has it's tag set to the Mastodon notification ID, and it's ID set
|
||||||
|
* to the ID of the account that received the notification.
|
||||||
*
|
*
|
||||||
* @param context to access application preferences and services
|
* @param context to access application preferences and services
|
||||||
* @param body a new Mastodon notification
|
* @param body a new Mastodon notification
|
||||||
* @param account the account for which the notification should be shown
|
* @param account the account for which the notification should be shown
|
||||||
|
* @return the new notification
|
||||||
*/
|
*/
|
||||||
|
@NonNull
|
||||||
public static void make(final Context context, Notification body, AccountEntity account, boolean isFirstOfBatch) {
|
public static android.app.Notification make(final Context context, NotificationManager notificationManager, Notification body, AccountEntity account, boolean isFirstOfBatch) {
|
||||||
body = body.rewriteToStatusTypeIfNeeded(account.getAccountId());
|
body = body.rewriteToStatusTypeIfNeeded(account.getAccountId());
|
||||||
|
String mastodonNotificationId = body.getId();
|
||||||
|
int accountId = (int) account.getId();
|
||||||
|
|
||||||
if (!filterNotification(account, body, context)) {
|
// Check for an existing notification with this Mastodon Notification ID
|
||||||
return;
|
android.app.Notification existingAndroidNotification = null;
|
||||||
}
|
StatusBarNotification[] activeNotifications = notificationManager.getActiveNotifications();
|
||||||
|
for (StatusBarNotification androidNotification : activeNotifications) {
|
||||||
String rawCurrentNotifications = account.getActiveNotifications();
|
if (mastodonNotificationId.equals(androidNotification.getTag()) && accountId == androidNotification.getId()) {
|
||||||
JSONArray currentNotifications;
|
existingAndroidNotification = androidNotification.getNotification();
|
||||||
|
|
||||||
try {
|
|
||||||
currentNotifications = new JSONArray(rawCurrentNotifications);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
currentNotifications = new JSONArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < currentNotifications.length(); i++) {
|
|
||||||
try {
|
|
||||||
if (currentNotifications.getString(i).equals(body.getAccount().getName())) {
|
|
||||||
currentNotifications.remove(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.d(TAG, Log.getStackTraceString(e));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentNotifications.put(body.getAccount().getName());
|
|
||||||
|
|
||||||
account.setActiveNotifications(currentNotifications.toString());
|
|
||||||
|
|
||||||
// Notification group member
|
// Notification group member
|
||||||
// =========================
|
// =========================
|
||||||
final NotificationCompat.Builder builder = newNotification(context, body, account, false);
|
|
||||||
|
|
||||||
notificationId++;
|
notificationId++;
|
||||||
|
// Create the notification -- either create a new one, or use the existing one.
|
||||||
|
NotificationCompat.Builder builder;
|
||||||
|
if (existingAndroidNotification == null) {
|
||||||
|
builder = newAndroidNotification(context, body, account);
|
||||||
|
} else {
|
||||||
|
builder = new NotificationCompat.Builder(context, existingAndroidNotification);
|
||||||
|
}
|
||||||
|
|
||||||
builder.setContentTitle(titleForType(context, body, account))
|
builder.setContentTitle(titleForType(context, body, account))
|
||||||
.setContentText(bodyForType(body, context, account.getAlwaysOpenSpoiler()));
|
.setContentText(bodyForType(body, context, account.getAlwaysOpenSpoiler()));
|
||||||
|
@ -233,51 +234,136 @@ public class NotificationHelper {
|
||||||
builder.setCategory(NotificationCompat.CATEGORY_SOCIAL);
|
builder.setCategory(NotificationCompat.CATEGORY_SOCIAL);
|
||||||
builder.setOnlyAlertOnce(true);
|
builder.setOnlyAlertOnce(true);
|
||||||
|
|
||||||
// only alert for the first notification of a batch to avoid multiple alerts at once
|
Bundle extras = new Bundle();
|
||||||
|
// Add the sending account's name, so it can be used when summarising this notification
|
||||||
|
extras.putString(EXTRA_ACCOUNT_NAME, body.getAccount().getName());
|
||||||
|
extras.putString(EXTRA_NOTIFICATION_TYPE, body.getType().toString());
|
||||||
|
builder.addExtras(extras);
|
||||||
|
|
||||||
|
// Only alert for the first notification of a batch to avoid multiple alerts at once
|
||||||
if(!isFirstOfBatch) {
|
if(!isFirstOfBatch) {
|
||||||
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY);
|
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary
|
return builder.build();
|
||||||
final NotificationCompat.Builder summaryBuilder = newNotification(context, body, account, true);
|
}
|
||||||
|
|
||||||
if (currentNotifications.length() != 1) {
|
/**
|
||||||
try {
|
* Updates the summary notifications for each notification group.
|
||||||
String title = context.getResources().getQuantityString(R.plurals.notification_title_summary, currentNotifications.length(), currentNotifications.length());
|
* <p>
|
||||||
String text = joinNames(context, currentNotifications);
|
* Notifications are sent to channels. Within each channel they may be grouped, and the group
|
||||||
summaryBuilder.setContentTitle(title)
|
* may have a summary.
|
||||||
.setContentText(text);
|
* <p>
|
||||||
} catch (JSONException e) {
|
* Tusky uses N notification channels for each account, each channel corresponds to a type
|
||||||
Log.d(TAG, Log.getStackTraceString(e));
|
* of notification (follow, reblog, mention, etc). Therefore each channel also has exactly
|
||||||
}
|
* 0 or 1 summary notifications along with its regular notifications.
|
||||||
|
* <p>
|
||||||
|
* The group key is the same as the channel ID.
|
||||||
|
* <p>
|
||||||
|
* Regnerates the summary notifications for all active Tusky notifications for `account`.
|
||||||
|
* This may delete the summary notification if there are no active notifications for that
|
||||||
|
* account in a group.
|
||||||
|
*
|
||||||
|
* @see <a href="https://developer.android.com/develop/ui/views/notifications/group">Create a
|
||||||
|
* notification group</a>
|
||||||
|
* @param context to access application preferences and services
|
||||||
|
* @param notificationManager the system's NotificationManager
|
||||||
|
* @param account the account for which the notification should be shown
|
||||||
|
*/
|
||||||
|
public static void updateSummaryNotifications(Context context, NotificationManager notificationManager, AccountEntity account) {
|
||||||
|
// Map from the channel ID to a list of notifications in that channel. Those are the
|
||||||
|
// notifications that will be summarised.
|
||||||
|
Map<String, List<StatusBarNotification>> channelGroups = new HashMap<>();
|
||||||
|
int accountId = (int) account.getId();
|
||||||
|
|
||||||
|
// Initialise the map with all channel IDs.
|
||||||
|
for (Notification.Type ty : Notification.Type.values()) {
|
||||||
|
channelGroups.put(getChannelId(account, ty), new ArrayList<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
summaryBuilder.setSubText(account.getFullName());
|
// Fetch all existing notifications. Add them to the map, ignoring notifications that:
|
||||||
summaryBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE);
|
// - belong to a different account
|
||||||
summaryBuilder.setCategory(NotificationCompat.CATEGORY_SOCIAL);
|
// - are summary notifications
|
||||||
summaryBuilder.setOnlyAlertOnce(true);
|
for (StatusBarNotification sn : notificationManager.getActiveNotifications()) {
|
||||||
summaryBuilder.setGroupSummary(true);
|
if (sn.getId() != accountId) continue;
|
||||||
|
|
||||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
String channelId = sn.getNotification().getGroup();
|
||||||
|
String summaryTag = GROUP_SUMMARY_TAG + "." + channelId;
|
||||||
|
if (summaryTag.equals(sn.getTag())) continue;
|
||||||
|
|
||||||
notificationManager.notify(notificationId, builder.build());
|
// TODO: API 26 supports getting the channel ID directly (sn.getNotification().getChannelId()).
|
||||||
if (currentNotifications.length() == 1) {
|
// This works here because the channelId and the groupKey are the same.
|
||||||
notificationManager.notify((int) account.getId(), builder.setGroupSummary(true).build());
|
List<StatusBarNotification> members = channelGroups.get(channelId);
|
||||||
} else {
|
if (members == null) { // can't happen, but just in case...
|
||||||
notificationManager.notify((int) account.getId(), summaryBuilder.build());
|
Log.e(TAG, "members == null for channel ID " + channelId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
members.add(sn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create, update, or cancel the summary notifications for each group.
|
||||||
|
for (Map.Entry<String, List<StatusBarNotification>> channelGroup : channelGroups.entrySet()) {
|
||||||
|
String channelId = channelGroup.getKey();
|
||||||
|
List<StatusBarNotification> members = channelGroup.getValue();
|
||||||
|
String summaryTag = GROUP_SUMMARY_TAG + "." + channelId;
|
||||||
|
|
||||||
|
// If there are 0-1 notifications in this group then the additional summary
|
||||||
|
// notification is not needed and can be cancelled.
|
||||||
|
if (members.size() <= 1) {
|
||||||
|
notificationManager.cancel(summaryTag, accountId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a notification that summarises the other notifications in this group
|
||||||
|
|
||||||
|
// All notifications in this group have the same type, so get it from the first.
|
||||||
|
String notificationType = members.get(0).getNotification().extras.getString(EXTRA_NOTIFICATION_TYPE);
|
||||||
|
|
||||||
|
Intent summaryResultIntent = new Intent(context, MainActivity.class);
|
||||||
|
summaryResultIntent.putExtra(ACCOUNT_ID, (long) accountId);
|
||||||
|
summaryResultIntent.putExtra(TYPE, notificationType);
|
||||||
|
TaskStackBuilder summaryStackBuilder = TaskStackBuilder.create(context);
|
||||||
|
summaryStackBuilder.addParentStack(MainActivity.class);
|
||||||
|
summaryStackBuilder.addNextIntent(summaryResultIntent);
|
||||||
|
|
||||||
|
PendingIntent summaryResultPendingIntent = summaryStackBuilder.getPendingIntent((int) (notificationId + account.getId() * 10000),
|
||||||
|
pendingIntentFlags(false));
|
||||||
|
|
||||||
|
String title = context.getResources().getQuantityString(R.plurals.notification_title_summary, members.size(), members.size());
|
||||||
|
String text = joinNames(context, members);
|
||||||
|
|
||||||
|
NotificationCompat.Builder summaryBuilder = new NotificationCompat.Builder(context, channelId)
|
||||||
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
|
.setContentIntent(summaryResultPendingIntent)
|
||||||
|
.setColor(context.getColor(R.color.notification_color))
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setShortcutId(Long.toString(account.getId()))
|
||||||
|
.setDefaults(0) // So it doesn't ring twice, notify only in Target callback
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(text)
|
||||||
|
.setSubText(account.getFullName())
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setGroup(channelId)
|
||||||
|
.setGroupSummary(true);
|
||||||
|
|
||||||
|
setSoundVibrationLight(account, summaryBuilder);
|
||||||
|
|
||||||
|
// TODO: Use the batch notification API available in NotificationManagerCompat
|
||||||
|
// 1.11 and up (https://developer.android.com/jetpack/androidx/releases/core#1.11.0-alpha01)
|
||||||
|
// when it is released.
|
||||||
|
notificationManager.notify(summaryTag, accountId, summaryBuilder.build());
|
||||||
|
|
||||||
|
// Android will rate limit / drop notifications if they're posted too
|
||||||
|
// quickly. There is no indication to the user that this happened.
|
||||||
|
// See https://github.com/tuskyapp/Tusky/pull/3626#discussion_r1192963664
|
||||||
|
try { Thread.sleep(1000); } catch (InterruptedException ignored) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NotificationCompat.Builder newNotification(Context context, Notification body, AccountEntity account, boolean summary) {
|
|
||||||
Intent summaryResultIntent = new Intent(context, MainActivity.class);
|
|
||||||
summaryResultIntent.putExtra(ACCOUNT_ID, account.getId());
|
|
||||||
summaryResultIntent.putExtra(TYPE, body.getType().name());
|
|
||||||
TaskStackBuilder summaryStackBuilder = TaskStackBuilder.create(context);
|
|
||||||
summaryStackBuilder.addParentStack(MainActivity.class);
|
|
||||||
summaryStackBuilder.addNextIntent(summaryResultIntent);
|
|
||||||
|
|
||||||
PendingIntent summaryResultPendingIntent = summaryStackBuilder.getPendingIntent((int) (notificationId + account.getId() * 10000),
|
private static NotificationCompat.Builder newAndroidNotification(Context context, Notification body, AccountEntity account) {
|
||||||
pendingIntentFlags(false));
|
|
||||||
|
|
||||||
// we have to switch account here
|
// we have to switch account here
|
||||||
Intent eventResultIntent = new Intent(context, MainActivity.class);
|
Intent eventResultIntent = new Intent(context, MainActivity.class);
|
||||||
|
@ -290,22 +376,19 @@ public class NotificationHelper {
|
||||||
PendingIntent eventResultPendingIntent = eventStackBuilder.getPendingIntent((int) account.getId(),
|
PendingIntent eventResultPendingIntent = eventStackBuilder.getPendingIntent((int) account.getId(),
|
||||||
pendingIntentFlags(false));
|
pendingIntentFlags(false));
|
||||||
|
|
||||||
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
|
String channelId = getChannelId(account, body);
|
||||||
deleteIntent.putExtra(ACCOUNT_ID, account.getId());
|
assert channelId != null;
|
||||||
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, summary ? (int) account.getId() : notificationId, deleteIntent,
|
|
||||||
pendingIntentFlags(false));
|
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getChannelId(account, body))
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
|
||||||
.setSmallIcon(R.drawable.ic_notify)
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
.setContentIntent(summary ? summaryResultPendingIntent : eventResultPendingIntent)
|
.setContentIntent(eventResultPendingIntent)
|
||||||
.setDeleteIntent(deletePendingIntent)
|
|
||||||
.setColor(context.getColor(R.color.notification_color))
|
.setColor(context.getColor(R.color.notification_color))
|
||||||
.setGroup(account.getAccountId())
|
.setGroup(channelId)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setShortcutId(Long.toString(account.getId()))
|
.setShortcutId(Long.toString(account.getId()))
|
||||||
.setDefaults(0); // So it doesn't ring twice, notify only in Target callback
|
.setDefaults(0); // So it doesn't ring twice, notify only in Target callback
|
||||||
|
|
||||||
setupPreferences(account, builder);
|
setSoundVibrationLight(account, builder);
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
@ -453,7 +536,6 @@ public class NotificationHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.createNotificationChannels(channels);
|
notificationManager.createNotificationChannels(channels);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -495,6 +577,15 @@ public class NotificationHelper {
|
||||||
WorkManager workManager = WorkManager.getInstance(context);
|
WorkManager workManager = WorkManager.getInstance(context);
|
||||||
workManager.cancelAllWorkByTag(NOTIFICATION_PULL_TAG);
|
workManager.cancelAllWorkByTag(NOTIFICATION_PULL_TAG);
|
||||||
|
|
||||||
|
// Periodic work requests are supposed to start running soon after being enqueued. In
|
||||||
|
// practice that may not be soon enough, so create and enqueue an expedited one-time
|
||||||
|
// request to get new notifications immediately.
|
||||||
|
WorkRequest fetchNotifications = new OneTimeWorkRequest.Builder(NotificationWorker.class)
|
||||||
|
.setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST)
|
||||||
|
.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||||
|
.build();
|
||||||
|
workManager.enqueue(fetchNotifications);
|
||||||
|
|
||||||
WorkRequest workRequest = new PeriodicWorkRequest.Builder(
|
WorkRequest workRequest = new PeriodicWorkRequest.Builder(
|
||||||
NotificationWorker.class,
|
NotificationWorker.class,
|
||||||
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS,
|
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS,
|
||||||
|
@ -502,6 +593,7 @@ public class NotificationHelper {
|
||||||
)
|
)
|
||||||
.addTag(NOTIFICATION_PULL_TAG)
|
.addTag(NOTIFICATION_PULL_TAG)
|
||||||
.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||||
|
.setInitialDelay(5, TimeUnit.MINUTES)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
workManager.enqueue(workRequest);
|
workManager.enqueue(workRequest);
|
||||||
|
@ -516,31 +608,23 @@ public class NotificationHelper {
|
||||||
|
|
||||||
public static void clearNotificationsForActiveAccount(@NonNull Context context, @NonNull AccountManager accountManager) {
|
public static void clearNotificationsForActiveAccount(@NonNull Context context, @NonNull AccountManager accountManager) {
|
||||||
AccountEntity account = accountManager.getActiveAccount();
|
AccountEntity account = accountManager.getActiveAccount();
|
||||||
if (account != null && !account.getActiveNotifications().equals("[]")) {
|
if (account == null) return;
|
||||||
Single.fromCallable(() -> {
|
int accountId = (int) account.getId();
|
||||||
account.setActiveNotifications("[]");
|
|
||||||
accountManager.saveAccount(account);
|
|
||||||
|
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
notificationManager.cancel((int) account.getId());
|
for (StatusBarNotification androidNotification : notificationManager.getActiveNotifications()) {
|
||||||
return true;
|
if (accountId == androidNotification.getId()) {
|
||||||
})
|
notificationManager.cancel(androidNotification.getTag(), androidNotification.getId());
|
||||||
.subscribeOn(Schedulers.io())
|
}
|
||||||
.subscribe();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean filterNotification(AccountEntity account, Notification notification,
|
public static boolean filterNotification(NotificationManager notificationManager, AccountEntity account, @NonNull Notification notification) {
|
||||||
Context context) {
|
return filterNotification(notificationManager, account, notification.getType());
|
||||||
return filterNotification(account, notification.getType(), context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean filterNotification(AccountEntity account, Notification.Type type,
|
public static boolean filterNotification(@NonNull NotificationManager notificationManager, @NonNull AccountEntity account, @NonNull Notification.Type type) {
|
||||||
Context context) {
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
|
|
||||||
String channelId = getChannelId(account, type);
|
String channelId = getChannelId(account, type);
|
||||||
if(channelId == null) {
|
if(channelId == null) {
|
||||||
// unknown notificationtype
|
// unknown notificationtype
|
||||||
|
@ -610,9 +694,7 @@ public class NotificationHelper {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setupPreferences(AccountEntity account,
|
private static void setSoundVibrationLight(AccountEntity account, NotificationCompat.Builder builder) {
|
||||||
NotificationCompat.Builder builder) {
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
return; //do nothing on Android O or newer, the system uses the channel settings anyway
|
return; //do nothing on Android O or newer, the system uses the channel settings anyway
|
||||||
}
|
}
|
||||||
|
@ -630,28 +712,29 @@ public class NotificationHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String wrapItemAt(JSONArray array, int index) throws JSONException {
|
private static String wrapItemAt(StatusBarNotification notification) {
|
||||||
return StringUtils.unicodeWrap(array.get(index).toString());
|
return StringUtils.unicodeWrap(notification.getNotification().extras.getString(EXTRA_ACCOUNT_NAME));//getAccount().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static String joinNames(Context context, JSONArray array) throws JSONException {
|
private static String joinNames(Context context, List<StatusBarNotification> notifications) {
|
||||||
if (array.length() > 3) {
|
if (notifications.size() > 3) {
|
||||||
int length = array.length();
|
int length = notifications.size();
|
||||||
|
//notifications.get(0).getNotification().extras.getString(EXTRA_ACCOUNT_NAME);
|
||||||
return String.format(context.getString(R.string.notification_summary_large),
|
return String.format(context.getString(R.string.notification_summary_large),
|
||||||
wrapItemAt(array, length - 1),
|
wrapItemAt(notifications.get(length - 1)),
|
||||||
wrapItemAt(array, length - 2),
|
wrapItemAt(notifications.get(length - 2)),
|
||||||
wrapItemAt(array, length - 3),
|
wrapItemAt(notifications.get(length - 3)),
|
||||||
length - 3);
|
length - 3);
|
||||||
} else if (array.length() == 3) {
|
} else if (notifications.size() == 3) {
|
||||||
return String.format(context.getString(R.string.notification_summary_medium),
|
return String.format(context.getString(R.string.notification_summary_medium),
|
||||||
wrapItemAt(array, 2),
|
wrapItemAt(notifications.get(2)),
|
||||||
wrapItemAt(array, 1),
|
wrapItemAt(notifications.get(1)),
|
||||||
wrapItemAt(array, 0));
|
wrapItemAt(notifications.get(0)));
|
||||||
} else if (array.length() == 2) {
|
} else if (notifications.size() == 2) {
|
||||||
return String.format(context.getString(R.string.notification_summary_small),
|
return String.format(context.getString(R.string.notification_summary_small),
|
||||||
wrapItemAt(array, 1),
|
wrapItemAt(notifications.get(1)),
|
||||||
wrapItemAt(array, 0));
|
wrapItemAt(notifications.get(0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -151,8 +151,9 @@ fun disableAllNotifications(context: Context, accountManager: AccountManager) {
|
||||||
|
|
||||||
private fun buildSubscriptionData(context: Context, account: AccountEntity): Map<String, Boolean> =
|
private fun buildSubscriptionData(context: Context, account: AccountEntity): Map<String, Boolean> =
|
||||||
buildMap {
|
buildMap {
|
||||||
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
Notification.Type.visibleTypes.forEach {
|
Notification.Type.visibleTypes.forEach {
|
||||||
put("data[alerts][${it.presentation}]", NotificationHelper.filterNotification(account, it, context))
|
put("data[alerts][${it.presentation}]", NotificationHelper.filterNotification(notificationManager, account, it))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,6 @@ data class AccountEntity(
|
||||||
*/
|
*/
|
||||||
var mediaPreviewEnabled: Boolean = true,
|
var mediaPreviewEnabled: Boolean = true,
|
||||||
var lastNotificationId: String = "0",
|
var lastNotificationId: String = "0",
|
||||||
var activeNotifications: String = "[]",
|
|
||||||
var emojis: List<Emoji> = emptyList(),
|
var emojis: List<Emoji> = emptyList(),
|
||||||
var tabPreferences: List<TabData> = defaultTabs(),
|
var tabPreferences: List<TabData> = defaultTabs(),
|
||||||
var notificationsFilter: String = "[\"follow_request\"]",
|
var notificationsFilter: String = "[\"follow_request\"]",
|
||||||
|
|
|
@ -18,7 +18,9 @@ package com.keylesspalace.tusky.db;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.room.AutoMigration;
|
import androidx.room.AutoMigration;
|
||||||
import androidx.room.Database;
|
import androidx.room.Database;
|
||||||
|
import androidx.room.DeleteColumn;
|
||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
|
import androidx.room.migration.AutoMigrationSpec;
|
||||||
import androidx.room.migration.Migration;
|
import androidx.room.migration.Migration;
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
|
||||||
|
@ -39,9 +41,10 @@ import java.io.File;
|
||||||
TimelineAccountEntity.class,
|
TimelineAccountEntity.class,
|
||||||
ConversationEntity.class
|
ConversationEntity.class
|
||||||
},
|
},
|
||||||
version = 49,
|
version = 50,
|
||||||
autoMigrations = {
|
autoMigrations = {
|
||||||
@AutoMigration(from = 48, to = 49)
|
@AutoMigration(from = 48, to = 49),
|
||||||
|
@AutoMigration(from = 49, to = 50, spec = AppDatabase.MIGRATION_49_50.class)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
@ -665,4 +668,7 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `filtered` TEXT");
|
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `filtered` TEXT");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@DeleteColumn(tableName = "AccountEntity", columnName = "activeNotifications")
|
||||||
|
static class MIGRATION_49_50 implements AutoMigrationSpec { }
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
package com.keylesspalace.tusky.di
|
package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
import com.keylesspalace.tusky.receiver.NotificationBlockStateBroadcastReceiver
|
import com.keylesspalace.tusky.receiver.NotificationBlockStateBroadcastReceiver
|
||||||
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver
|
|
||||||
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver
|
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver
|
||||||
import com.keylesspalace.tusky.receiver.UnifiedPushBroadcastReceiver
|
import com.keylesspalace.tusky.receiver.UnifiedPushBroadcastReceiver
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
@ -28,9 +27,6 @@ abstract class BroadcastReceiverModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributeSendStatusBroadcastReceiver(): SendStatusBroadcastReceiver
|
abstract fun contributeSendStatusBroadcastReceiver(): SendStatusBroadcastReceiver
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
|
||||||
abstract fun contributeNotificationClearBroadcastReceiver(): NotificationClearBroadcastReceiver
|
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributeUnifiedPushBroadcastReceiver(): UnifiedPushBroadcastReceiver
|
abstract fun contributeUnifiedPushBroadcastReceiver(): UnifiedPushBroadcastReceiver
|
||||||
|
|
||||||
|
|
|
@ -152,11 +152,19 @@ interface MastodonApi {
|
||||||
@Query("timeline[]") timelines: List<String>
|
@Query("timeline[]") timelines: List<String>
|
||||||
): Single<Map<String, Marker>>
|
): Single<Map<String, Marker>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("api/v1/markers")
|
||||||
|
fun updateMarkersWithAuth(
|
||||||
|
@Header("Authorization") auth: String,
|
||||||
|
@Field("home[last_read_id]") homeLastReadId: String? = null,
|
||||||
|
@Field("notifications[last_read_id]") notificationsLastReadId: String? = null
|
||||||
|
): NetworkResult<Unit>
|
||||||
|
|
||||||
@GET("api/v1/notifications")
|
@GET("api/v1/notifications")
|
||||||
fun notificationsWithAuth(
|
fun notificationsWithAuth(
|
||||||
@Header("Authorization") auth: String,
|
@Header("Authorization") auth: String,
|
||||||
@Header(DOMAIN_HEADER) domain: String,
|
@Header(DOMAIN_HEADER) domain: String,
|
||||||
@Query("since_id") sinceId: String?
|
@Query("min_id") minId: String?
|
||||||
): Single<List<Notification>>
|
): Single<List<Notification>>
|
||||||
|
|
||||||
@POST("api/v1/notifications/clear")
|
@POST("api/v1/notifications/clear")
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
/* Copyright 2018 Conny Duck
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky.receiver
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
|
||||||
import dagger.android.AndroidInjection
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class NotificationClearBroadcastReceiver : BroadcastReceiver() {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var accountManager: AccountManager
|
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
AndroidInjection.inject(this, context)
|
|
||||||
|
|
||||||
val accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1)
|
|
||||||
|
|
||||||
val account = accountManager.getAccountById(accountId)
|
|
||||||
if (account != null) {
|
|
||||||
account.activeNotifications = "[]"
|
|
||||||
accountManager.saveAccount(account)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -90,8 +90,9 @@ class MainActivityTest {
|
||||||
NotificationHelper.createNotificationChannelsForAccount(accountEntity, context)
|
NotificationHelper.createNotificationChannelsForAccount(accountEntity, context)
|
||||||
|
|
||||||
runInBackground {
|
runInBackground {
|
||||||
NotificationHelper.make(
|
val notification = NotificationHelper.make(
|
||||||
context,
|
context,
|
||||||
|
notificationManager,
|
||||||
Notification(
|
Notification(
|
||||||
type = type,
|
type = type,
|
||||||
id = "id",
|
id = "id",
|
||||||
|
@ -110,6 +111,7 @@ class MainActivityTest {
|
||||||
accountEntity,
|
accountEntity,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
notificationManager.notify("id", 1, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
val notification = shadowNotificationManager.allNotifications.first()
|
val notification = shadowNotificationManager.allNotifications.first()
|
||||||
|
|
Loading…
Reference in a new issue