From 117c9fa0c2feb72cf58b5372d2e850dce3ef8690 Mon Sep 17 00:00:00 2001 From: Lakoja Date: Thu, 6 Apr 2023 14:13:10 +0200 Subject: [PATCH 01/12] 3516: (working) Prototype of a DB api call logger --- .../49.json | 1057 +++++++++++++++++ .../keylesspalace/tusky/TuskyApplication.kt | 13 + .../keylesspalace/tusky/db/AppDatabase.java | 20 +- .../com/keylesspalace/tusky/db/Converters.kt | 11 + .../keylesspalace/tusky/db/OccurrenceDao.kt | 36 + .../tusky/db/OccurrenceEntity.kt | 72 ++ .../com/keylesspalace/tusky/di/AppModule.kt | 2 +- .../keylesspalace/tusky/di/NetworkModule.kt | 9 +- .../tusky/network/LogToDbInterceptor.kt | 81 ++ 9 files changed, 1297 insertions(+), 4 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/49.json create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/db/OccurrenceEntity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/network/LogToDbInterceptor.kt diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/49.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/49.json new file mode 100644 index 0000000000..3e0a26c2ce --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/49.json @@ -0,0 +1,1057 @@ +{ + "formatVersion": 1, + "database": { + "version": 49, + "identityHash": "e5748dfe3c822a1305530005ac41f534", + "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, `activeNotifications` 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)", + "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": "activeNotifications", + "columnName": "activeNotifications", + "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 + } + ], + "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": [] + }, + { + "tableName": "OccurrenceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER, `type` TEXT NOT NULL, `what` TEXT NOT NULL, `startedAt` INTEGER NOT NULL, `finishedAt` INTEGER, `code` INTEGER, `callTrace` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "what", + "columnName": "what", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startedAt", + "columnName": "startedAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "finishedAt", + "columnName": "finishedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "code", + "columnName": "code", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "callTrace", + "columnName": "callTrace", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "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, 'e5748dfe3c822a1305530005ac41f534')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index ef5b8cab44..47dbf70892 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -21,6 +21,7 @@ import android.util.Log import androidx.work.WorkManager import autodispose2.AutoDisposePlugins import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory +import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.di.AppInjector import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.SCHEMA_VERSION @@ -50,6 +51,9 @@ class TuskyApplication : Application(), HasAndroidInjector { @Inject lateinit var sharedPreferences: SharedPreferences + @Inject + lateinit var db: AppDatabase + override fun onCreate() { // Uncomment me to get StrictMode violation logs // if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { @@ -63,6 +67,15 @@ class TuskyApplication : Application(), HasAndroidInjector { // } super.onCreate() + val existingUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler { t, e -> +// Log.e(TAG, "There exception "+e) + + // TODO db.occurrenceDao().insertOrReplace() ; OccurrenceEntity.reduceTrace(e.stackTrace) + + existingUncaughtHandler?.uncaughtException(t, e) + } + Security.insertProviderAt(Conscrypt.newProvider(), 1) AutoDisposePlugins.setHideProxies(false) // a small performance optimization diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 3225cad41b..3990015b77 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -30,8 +30,8 @@ * DB version & declare DAO */ @Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, - TimelineAccountEntity.class, ConversationEntity.class - }, version = 48) + TimelineAccountEntity.class, ConversationEntity.class, OccurrenceEntity.class + }, version = 49) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -39,6 +39,7 @@ public abstract class AppDatabase extends RoomDatabase { public abstract ConversationsDao conversationDao(); public abstract TimelineDao timelineDao(); public abstract DraftDao draftDao(); + public abstract OccurrenceDao occurrenceDao(); public static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override @@ -653,4 +654,19 @@ public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `filtered` TEXT"); } }; + + public static final Migration MIGRATION_48_49 = new Migration(48, 49) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `OccurrenceEntity` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," + + "`accountId` INTEGER," + + "`type` TEXT NOT NULL," + + "`what` TEXT NOT NULL," + + "`startedAt` INTEGER NOT NULL," + + "`finishedAt` INTEGER," + + "`code` INTEGER," + + "`callTrace` TEXT NOT NULL)"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt index 6ef9425452..b9de894ee8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -175,4 +175,15 @@ class Converters @Inject constructor( fun jsonToFilterResultList(filterResultListJson: String?): List? { return gson.fromJson(filterResultListJson, object : TypeToken>() {}.type) } + + // TODO this (simple enum <-> string) does not work automatically? + @TypeConverter + fun occurrenceTypeToString(type: OccurrenceEntity.Type): String { + return type.name + } + + @TypeConverter + fun stringToOccurrenceType(type: String): OccurrenceEntity.Type { + return OccurrenceEntity.Type.fromString(type) + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt new file mode 100644 index 0000000000..c7b3996f89 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt @@ -0,0 +1,36 @@ +/* Copyright Tusky Contributors + * + * 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 . */ + +package com.keylesspalace.tusky.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface OccurrenceDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertOrReplace(one: OccurrenceEntity): Long + +// @Query("SELECT * FROM OccurrenceEntity WHERE accountId = :accountId ORDER BY id ASC") +// fun pagingSource(accountId: Long): PagingSource + + @Query("SELECT * FROM OccurrenceEntity WHERE accountId = :accountId") + suspend fun loadAll(accountId: Long): List + + @Query("DELETE FROM OccurrenceEntity WHERE id = :id") + suspend fun delete(id: Int) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceEntity.kt new file mode 100644 index 0000000000..c7674902df --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceEntity.kt @@ -0,0 +1,72 @@ +/* Copyright Tusky Contributors + * + * 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 . */ + +package com.keylesspalace.tusky.db + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import java.util.* + +@Entity +@TypeConverters(Converters::class) +data class OccurrenceEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val accountId: Long? = null, + val type: Type, + val what: String, + val startedAt: Date, // TODO or use LocalDateTime (or Long)? + val finishedAt: Date? = null, + val code: Int? = null, + val callTrace: String, +) { + companion object { + fun reduceTrace(stackTrace: Array): String { + // TODO conditions/transforms here are a bit arbitrary... + // TODO we could maybe record more (full stack?) in the db and only reduce it for display? + // TODO probably keep at least the last non-Tusky location in the stack; and/or keep the information that some entries were removed + + var tuskyTrace = stackTrace.filter { it.className.startsWith("com.keylesspalace.tusky") && !it.methodName.contains("intercept") } + if (tuskyTrace.size > 3) { + tuskyTrace = tuskyTrace.subList(0, 3) + } + + return tuskyTrace.joinToString("<") { reduceClassName(it.className) + "." + it.methodName + "():" + it.lineNumber } + } + + private fun reduceClassName(className: String): String { + return className.substringAfter("com.keylesspalace.tusky.") + +// if (!className.contains('.')) { +// return className +// } +// +// val parts = className.split('.') +// +// return parts.subList(parts.size-2, parts.size).joinToString(".") + } + } + + enum class Type { + APICALL, + CRASH; + + companion object { + fun fromString(type: String): Type { + return values().first { it.name.equals(type, true) } + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index bc2c7d7537..5b9a35e966 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -68,7 +68,7 @@ class AppModule { AppDatabase.MIGRATION_38_39, AppDatabase.MIGRATION_39_40, AppDatabase.MIGRATION_40_41, AppDatabase.MIGRATION_41_42, AppDatabase.MIGRATION_42_43, AppDatabase.MIGRATION_43_44, AppDatabase.MIGRATION_44_45, AppDatabase.MIGRATION_45_46, AppDatabase.MIGRATION_46_47, - AppDatabase.MIGRATION_47_48 + AppDatabase.MIGRATION_47_48, AppDatabase.MIGRATION_48_49 ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index 03b4ad3943..b782cbabb4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -24,8 +24,10 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.json.Rfc3339DateJsonAdapter import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor +import com.keylesspalace.tusky.network.LogToDbInterceptor import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MediaUploadApi import com.keylesspalace.tusky.settings.PrefKeys.HTTP_PROXY_ENABLED @@ -68,7 +70,8 @@ class NetworkModule { fun providesHttpClient( accountManager: AccountManager, context: Context, - preferences: SharedPreferences + preferences: SharedPreferences, + db: AppDatabase ): OkHttpClient { val httpProxyEnabled = preferences.getBoolean(HTTP_PROXY_ENABLED, false) val httpServer = preferences.getNonNullString(HTTP_PROXY_SERVER, "") @@ -105,6 +108,10 @@ class NetworkModule { addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) if (BuildConfig.DEBUG) { addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }) +// System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON) +// System.setProperty(STACKTRACE_RECOVERY_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON) + addInterceptor(LogToDbInterceptor(db.occurrenceDao(), accountManager.activeAccount?.id)) + // TODO probably not really the correct location / should be near Api? The account id here could be wrong with multiple reasons. } } .build() diff --git a/app/src/main/java/com/keylesspalace/tusky/network/LogToDbInterceptor.kt b/app/src/main/java/com/keylesspalace/tusky/network/LogToDbInterceptor.kt new file mode 100644 index 0000000000..586b180aeb --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/LogToDbInterceptor.kt @@ -0,0 +1,81 @@ +/* Copyright Tusky Contributors + * + * 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 . */ + +package com.keylesspalace.tusky.network + +import com.keylesspalace.tusky.db.OccurrenceDao +import com.keylesspalace.tusky.db.OccurrenceEntity +import kotlinx.coroutines.runBlocking +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import java.io.IOException +import java.util.Calendar +import java.util.concurrent.TimeUnit + +class LogToDbInterceptor(private val dao: OccurrenceDao, private val accountId: Long?) : Interceptor { + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request: Request = chain.request() + + val startTime = Calendar.getInstance().time + + val what = OccurrenceEntity( + accountId = accountId, + type = OccurrenceEntity.Type.APICALL, + what = request.method + " " + request.url.toString(), + startedAt = startTime, + callTrace = OccurrenceEntity.reduceTrace(Throwable().stackTrace), + ) + // TODO all stack traces here have no hint where they might have originated (always ThreadPool) + // found kotlinx.coroutines.stacktrace.recovery but that should be on by default? + + val entityId: Long + runBlocking { + // TODO runBlocking is the right thing to do here? + entityId = dao.insertOrReplace(what) + } + + val startNs = System.nanoTime() + val response: Response + try { + response = chain.proceed(request) + } catch (e: Exception) { + throw e + } + + val finishTime = Calendar.getInstance().time + + val afterWhat = what.copy( + id = entityId, + finishedAt = finishTime, + code = response.code, + ) + + runBlocking { + dao.insertOrReplace(afterWhat) + } + + + val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) + + val responseBody = response.body!! + val contentLength = responseBody.contentLength() + + +// network.LogToDbInterceptor.intercept:25; network.InstanceSwitchAuthInterceptor.intercept:70; di.NetworkModule$providesHttpClient$$inlined$-addInterceptor$1.intercept:1086 + return response + } +} From a5610e207be13374dc3248bfc132aa5334ebaa1b Mon Sep 17 00:00:00 2001 From: Lakoja Date: Thu, 6 Apr 2023 16:28:05 +0200 Subject: [PATCH 02/12] 3516: Add an activity to display occurrences (called from context menu) --- app/src/main/AndroidManifest.xml | 1 + .../keylesspalace/tusky/OccurrenceActivity.kt | 155 ++++++++++++++++++ .../components/timeline/TimelineFragment.kt | 13 ++ .../keylesspalace/tusky/db/OccurrenceDao.kt | 2 +- .../tusky/di/ActivitiesModule.kt | 4 + .../tusky/viewmodel/OccurrencesViewModel.kt | 30 ++++ .../main/res/layout/activity_occurrences.xml | 56 +++++++ app/src/main/res/layout/item_occurrence.xml | 39 +++++ app/src/main/res/menu/fragment_timeline.xml | 4 + app/src/main/res/values/strings.xml | 3 + 10 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/viewmodel/OccurrencesViewModel.kt create mode 100644 app/src/main/res/layout/activity_occurrences.xml create mode 100644 app/src/main/res/layout/item_occurrence.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0eaaf655f7..4a27e245cd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -153,6 +153,7 @@ + diff --git a/app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt new file mode 100644 index 0000000000..0c5ef67fcb --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt @@ -0,0 +1,155 @@ +/* Copyright Tusky Contributors + * + * 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 . + */ + +package com.keylesspalace.tusky + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.ListAdapter +import com.keylesspalace.tusky.databinding.ActivityOccurrencesBinding +import com.keylesspalace.tusky.databinding.ItemOccurrenceBinding +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.OccurrenceEntity +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.getRelativeTimeSpanString +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector +import kotlinx.coroutines.runBlocking +import java.text.DateFormat +import javax.inject.Inject + +// TODO should all this be in components/occurrence ? + +class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + // TODO what's this? + @Inject + lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector + + @Inject + lateinit var db: AppDatabase + + @Inject + lateinit var accountManager: AccountManager + +// private val viewModel: ListsViewModel by viewModels { viewModelFactory } + + private val binding by viewBinding(ActivityOccurrencesBinding::inflate) + + private val adapter = OccurrenceAdapter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.apply { + title = getString(R.string.title_occurrences) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + binding.occurrenceList.adapter = adapter + binding.occurrenceList.addItemDecoration( + DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + ) + + binding.swipeRefreshLayout.setOnRefreshListener { load() } + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + + // It's only function here so far: show "there is nothing" + binding.messageView.setup( + R.drawable.elephant_friend_empty, + R.string.message_empty, + null + ) + + load() + } + + private fun load() { +// if (binding.swipeRefreshLayout.isRefreshing) { +// return +// } + + // TODO well... + runBlocking { + accountManager.activeAccount?.let { + binding.swipeRefreshLayout.isRefreshing = true + + val occurrences = db.occurrenceDao().loadAll(it.id) + adapter.submitList(occurrences) + + binding.messageView.visible(occurrences.isEmpty()) + binding.occurrenceList.visible(occurrences.isNotEmpty()) + + binding.swipeRefreshLayout.isRefreshing = false + } + } + } + + override fun androidInjector() = dispatchingAndroidInjector + + companion object { + fun newIntent(context: Context) = Intent(context, OccurrenceActivity::class.java) + } + + private object OccurrenceDiffer : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: OccurrenceEntity, newItem: OccurrenceEntity): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: OccurrenceEntity, newItem: OccurrenceEntity): Boolean { + return oldItem == newItem + } + } + + private inner class OccurrenceAdapter : + ListAdapter>(OccurrenceDiffer) { + + private val dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + return BindingHolder(ItemOccurrenceBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + } + + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + val occurrence = getItem(position) + + holder.binding.code.text = occurrence.code.toString() + holder.binding.what.text = occurrence.what + holder.binding.whenDate.text = + getRelativeTimeSpanString(this@OccurrenceActivity.applicationContext, occurrence.startedAt.time, System.currentTimeMillis()) + //dateFormat.format(occurrence.startedAt) + + // TODO or AbsoluteTimeFormatter? + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt index c5998f1d3c..adeb840737 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt @@ -15,6 +15,7 @@ package com.keylesspalace.tusky.components.timeline +import android.content.Intent import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -40,6 +41,8 @@ import at.connyduck.sparkbutton.helpers.Utils import autodispose2.androidx.lifecycle.autoDispose import com.google.android.material.color.MaterialColors import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.OccurrenceActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.appstore.EventHub @@ -335,6 +338,10 @@ class TimelineFragment : MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary) } } + + if (!BuildConfig.DEBUG) { + menu.removeItem(R.id.action_occurrences) + } } } @@ -350,6 +357,12 @@ class TimelineFragment : false } } + R.id.action_occurrences -> { + // TODO should/could be placed in main drawer? + startActivity(Intent(this.context, OccurrenceActivity::class.java)) + + true + } else -> false } } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt index c7b3996f89..c5e184cd5f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt @@ -28,7 +28,7 @@ interface OccurrenceDao { // @Query("SELECT * FROM OccurrenceEntity WHERE accountId = :accountId ORDER BY id ASC") // fun pagingSource(accountId: Long): PagingSource - @Query("SELECT * FROM OccurrenceEntity WHERE accountId = :accountId") + @Query("SELECT * FROM OccurrenceEntity WHERE accountId = :accountId ORDER BY startedAt DESC") suspend fun loadAll(accountId: Long): List @Query("DELETE FROM OccurrenceEntity WHERE id = :id") diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt index 2ceb97213b..e20703adcf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -21,6 +21,7 @@ import com.keylesspalace.tusky.EditProfileActivity import com.keylesspalace.tusky.LicenseActivity import com.keylesspalace.tusky.ListsActivity import com.keylesspalace.tusky.MainActivity +import com.keylesspalace.tusky.OccurrenceActivity import com.keylesspalace.tusky.SplashActivity import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.TabPreferenceActivity @@ -132,4 +133,7 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesEditFilterActivity(): EditFilterActivity + + @ContributesAndroidInjector + abstract fun contributesOccurrencesActivity(): OccurrenceActivity } diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/OccurrencesViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/OccurrencesViewModel.kt new file mode 100644 index 0000000000..9d22f2fdbd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/OccurrencesViewModel.kt @@ -0,0 +1,30 @@ +/* Copyright Tusky Contributors + * + * 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 . + */ + +package com.keylesspalace.tusky.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.keylesspalace.tusky.network.MastodonApi +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal class OccurrencesViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() { + fun load() { + viewModelScope.launch { + } + } +} diff --git a/app/src/main/res/layout/activity_occurrences.xml b/app/src/main/res/layout/activity_occurrences.xml new file mode 100644 index 0000000000..09fe377a78 --- /dev/null +++ b/app/src/main/res/layout/activity_occurrences.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_occurrence.xml b/app/src/main/res/layout/item_occurrence.xml new file mode 100644 index 0000000000..ae93425c5a --- /dev/null +++ b/app/src/main/res/layout/item_occurrence.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/menu/fragment_timeline.xml b/app/src/main/res/menu/fragment_timeline.xml index bf722917fe..f90dbb8f2b 100644 --- a/app/src/main/res/menu/fragment_timeline.xml +++ b/app/src/main/res/menu/fragment_timeline.xml @@ -5,4 +5,8 @@ android:id="@+id/action_refresh" android:title="@string/action_refresh" app:showAsAction="never" /> + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8aa90ccaf9..9a17794b21 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -812,4 +812,7 @@ you follow.\n\nTo explore accounts you can either discover them in one of the other timelines. For example the local timeline of your instance [iconics gmd_group]. Or you can search them by name [iconics gmd_search]; for example search for Tusky to find our Mastodon account. + + Occurrences + Occurrences From 02aa360adee64e52f8641fd48d809f918b9adc74 Mon Sep 17 00:00:00 2001 From: Lakoja Date: Fri, 7 Apr 2023 10:08:02 +0200 Subject: [PATCH 03/12] 3516: Improve item display (and colors) --- .../keylesspalace/tusky/OccurrenceActivity.kt | 57 +++++++++++++++++- .../com/keylesspalace/tusky/db/AccountDao.kt | 3 + .../tusky/util/TimestampUtils.kt | 13 ++++ app/src/main/res/layout/item_occurrence.xml | 60 +++++++++++++------ 4 files changed, 112 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt index 0c5ef67fcb..e6174ce9fa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt @@ -18,12 +18,15 @@ package com.keylesspalace.tusky import android.content.Context import android.content.Intent +import android.graphics.Color +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ListAdapter +import com.google.android.material.color.MaterialColors import com.keylesspalace.tusky.databinding.ActivityOccurrencesBinding import com.keylesspalace.tusky.databinding.ItemOccurrenceBinding import com.keylesspalace.tusky.db.AccountManager @@ -32,6 +35,7 @@ import com.keylesspalace.tusky.db.OccurrenceEntity import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.getDurationStringAllowMillis import com.keylesspalace.tusky.util.getRelativeTimeSpanString import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible @@ -143,13 +147,62 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { override fun onBindViewHolder(holder: BindingHolder, position: Int) { val occurrence = getItem(position) - holder.binding.code.text = occurrence.code.toString() + val defaultTextColor = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary) + holder.binding.what.text = occurrence.what + + holder.binding.code.text = occurrence.code.toString() + holder.binding.code.setTextColor( + if (occurrence.code != null && occurrence.code > 0) { + if (occurrence.code >= 400) { + Color.RED + } else if (occurrence.code >= 300) { + Color.YELLOW + } else { + Color.GREEN + } + } else { + defaultTextColor + } + ) + holder.binding.whenDate.text = getRelativeTimeSpanString(this@OccurrenceActivity.applicationContext, occurrence.startedAt.time, System.currentTimeMillis()) //dateFormat.format(occurrence.startedAt) - // TODO or AbsoluteTimeFormatter? + + // TODO how does one get the current locale /and/or format numbers here? + val currentLocale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + resources.configuration.locales[0] + } else { + resources.configuration.locale + } + + var duration = "" + var durationMs = 0L + if (occurrence.finishedAt != null) { + durationMs = occurrence.finishedAt.time - occurrence.startedAt.time + duration = getDurationStringAllowMillis(currentLocale, durationMs) + } + holder.binding.duration.text = duration + holder.binding.duration.setTextColor( + if (durationMs >= 1000) { + Color.RED + } else if (durationMs >= 400) { + Color.YELLOW + } else { + Color.GREEN + } + ) + + holder.binding.who.text = if (occurrence.accountId != null) { + val account = db.accountDao().get(occurrence.accountId) + account?.displayName ?: "" + } else { + "" + } + + // TODO cache some objects here? For example that account is probably always the same; or different helper objects (locale, number format, ...) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt index 218c9b8f4d..11650a9039 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountDao.kt @@ -23,6 +23,9 @@ import androidx.room.Query @Dao interface AccountDao { + @Query("SELECT * FROM AccountEntity WHERE id = :id LIMIT 1") + fun get(id: Long): AccountEntity? + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertOrReplace(account: AccountEntity): Long diff --git a/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt index a6717ed4fe..4c11b49f17 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt @@ -18,6 +18,8 @@ package com.keylesspalace.tusky.util import android.content.Context import com.keylesspalace.tusky.R +import java.text.NumberFormat +import java.util.* import kotlin.math.abs private const val SECOND_IN_MILLIS: Long = 1000 @@ -79,6 +81,17 @@ fun getRelativeTimeSpanString(context: Context, then: Long, now: Long): String { return context.getString(format, span) } +fun getDurationStringAllowMillis(locale: Locale, durationMs: Long): String { + val formatter = NumberFormat.getInstance(locale) + formatter.maximumFractionDigits = 1 + + return if (abs(durationMs) < SECOND_IN_MILLIS) { + formatter.format(durationMs) + "ms" + } else { + formatter.format(durationMs / 1000.0f) + "s" + } +} + fun formatPollDuration(context: Context, then: Long, now: Long): String { var span = then - now if (span < 0) { diff --git a/app/src/main/res/layout/item_occurrence.xml b/app/src/main/res/layout/item_occurrence.xml index ae93425c5a..97a3671ceb 100644 --- a/app/src/main/res/layout/item_occurrence.xml +++ b/app/src/main/res/layout/item_occurrence.xml @@ -3,37 +3,59 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:gravity="center_vertical" - android:orientation="horizontal"> + android:orientation="vertical" + android:layout_margin="6dp"> + android:id="@+id/what" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + android:textStyle="bold" + android:layout_marginBottom="4dp" + tools:text="Example crash" /> + android:orientation="horizontal"> + android:id="@+id/code" + android:layout_width="wrap_content" + android:minWidth="50dp" + android:layout_height="match_parent" + android:lines="1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + android:paddingEnd="6dp" + tools:text="500" /> + + + + From fbddaa300221ea632c610c6218459b98ac966e68 Mon Sep 17 00:00:00 2001 From: Lakoja Date: Fri, 7 Apr 2023 10:40:14 +0200 Subject: [PATCH 04/12] 3516: Support crashes (use the proper "root cause" there) --- .../keylesspalace/tusky/OccurrenceActivity.kt | 12 +++++++- .../keylesspalace/tusky/TuskyApplication.kt | 30 +++++++++++++++++-- app/src/main/res/layout/item_occurrence.xml | 10 +++++++ app/src/main/res/values/strings.xml | 2 +- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt index e6174ce9fa..2a402cb66b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt @@ -150,8 +150,15 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { val defaultTextColor = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary) holder.binding.what.text = occurrence.what + holder.binding.what.setTextColor( + if (occurrence.type == OccurrenceEntity.Type.CRASH) { + Color.RED + } else { + defaultTextColor + } + ) - holder.binding.code.text = occurrence.code.toString() + holder.binding.code.text = occurrence.code?.toString() ?: "" holder.binding.code.setTextColor( if (occurrence.code != null && occurrence.code > 0) { if (occurrence.code >= 400) { @@ -202,6 +209,9 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { "" } + holder.binding.trace.visible(occurrence.callTrace.isNotEmpty()) + holder.binding.trace.text = occurrence.callTrace // TODO this could/should be normal multi-line + // TODO cache some objects here? For example that account is probably always the same; or different helper objects (locale, number format, ...) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index 47dbf70892..b57ed47fb2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -21,7 +21,9 @@ import android.util.Log import androidx.work.WorkManager import autodispose2.AutoDisposePlugins import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory +import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.OccurrenceEntity import com.keylesspalace.tusky.di.AppInjector import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.SCHEMA_VERSION @@ -34,9 +36,12 @@ import de.c1710.filemojicompat_defaults.DefaultEmojiPackList import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper import de.c1710.filemojicompat_ui.helpers.EmojiPreference import io.reactivex.rxjava3.plugins.RxJavaPlugins +import kotlinx.coroutines.runBlocking import org.conscrypt.Conscrypt import java.security.Security +import java.util.* import javax.inject.Inject +import kotlin.math.min class TuskyApplication : Application(), HasAndroidInjector { @Inject @@ -54,6 +59,9 @@ class TuskyApplication : Application(), HasAndroidInjector { @Inject lateinit var db: AppDatabase + @Inject + lateinit var accountManager: AccountManager + override fun onCreate() { // Uncomment me to get StrictMode violation logs // if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { @@ -69,9 +77,25 @@ class TuskyApplication : Application(), HasAndroidInjector { val existingUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { t, e -> -// Log.e(TAG, "There exception "+e) - - // TODO db.occurrenceDao().insertOrReplace() ; OccurrenceEntity.reduceTrace(e.stackTrace) + runBlocking { + var rootCause = e + while (rootCause.cause != null && rootCause != rootCause.cause) { + rootCause = rootCause.cause!! + } + + val traceString = OccurrenceEntity.reduceTrace(rootCause.stackTrace) + var what = e.message + if (what == null && traceString.isNotEmpty()) { + what = traceString.substring(0, min(200, traceString.length)) + } + db.occurrenceDao().insertOrReplace(OccurrenceEntity( + accountId = accountManager.activeAccount?.id, + type = OccurrenceEntity.Type.CRASH, + what = what ?: "CRASH", + startedAt = Calendar.getInstance().time, + callTrace = traceString + )) + } existingUncaughtHandler?.uncaughtException(t, e) } diff --git a/app/src/main/res/layout/item_occurrence.xml b/app/src/main/res/layout/item_occurrence.xml index 97a3671ceb..d04c293bd3 100644 --- a/app/src/main/res/layout/item_occurrence.xml +++ b/app/src/main/res/layout/item_occurrence.xml @@ -58,4 +58,14 @@ android:textSize="?attr/status_text_medium" tools:text="Someone" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9a17794b21..666561cd1d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -813,6 +813,6 @@ For example the local timeline of your instance [iconics gmd_group]. Or you can search them by name [iconics gmd_search]; for example search for Tusky to find our Mastodon account. - Occurrences + Occurrences (event log) Occurrences From 720eb83f9318aac6989517d22a85ec1bd9900968 Mon Sep 17 00:00:00 2001 From: Lakoja Date: Fri, 7 Apr 2023 15:12:40 +0200 Subject: [PATCH 05/12] 3516: Move occurrence stuff to a new component --- app/src/main/AndroidManifest.xml | 2 +- .../keylesspalace/tusky/TuskyApplication.kt | 32 +----- .../occurrence/LogToDbInterceptor.kt | 44 +++++++ .../occurrence}/OccurrenceActivity.kt | 45 +++++--- .../occurrence}/OccurrenceEntity.kt | 3 +- .../occurrence/OccurrenceRepository.kt | 107 ++++++++++++++++++ .../occurrence}/OccurrencesViewModel.kt | 2 +- .../components/timeline/TimelineFragment.kt | 3 +- .../keylesspalace/tusky/db/AppDatabase.java | 1 + .../com/keylesspalace/tusky/db/Converters.kt | 1 + .../keylesspalace/tusky/db/OccurrenceDao.kt | 12 +- .../tusky/di/ActivitiesModule.kt | 2 +- .../keylesspalace/tusky/di/NetworkModule.kt | 11 +- .../tusky/network/LogToDbInterceptor.kt | 81 ------------- 14 files changed, 202 insertions(+), 144 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/occurrence/LogToDbInterceptor.kt rename app/src/main/java/com/keylesspalace/tusky/{ => components/occurrence}/OccurrenceActivity.kt (86%) rename app/src/main/java/com/keylesspalace/tusky/{db => components/occurrence}/OccurrenceEntity.kt (96%) create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt rename app/src/main/java/com/keylesspalace/tusky/{viewmodel => components/occurrence}/OccurrencesViewModel.kt (95%) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/network/LogToDbInterceptor.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4a27e245cd..3fffe07293 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -153,7 +153,7 @@ - + diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index b57ed47fb2..5354eb67ff 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -21,9 +21,7 @@ import android.util.Log import androidx.work.WorkManager import autodispose2.AutoDisposePlugins import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory -import com.keylesspalace.tusky.db.AccountManager -import com.keylesspalace.tusky.db.AppDatabase -import com.keylesspalace.tusky.db.OccurrenceEntity +import com.keylesspalace.tusky.components.occurrence.OccurrenceRepository import com.keylesspalace.tusky.di.AppInjector import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.SCHEMA_VERSION @@ -36,12 +34,9 @@ import de.c1710.filemojicompat_defaults.DefaultEmojiPackList import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper import de.c1710.filemojicompat_ui.helpers.EmojiPreference import io.reactivex.rxjava3.plugins.RxJavaPlugins -import kotlinx.coroutines.runBlocking import org.conscrypt.Conscrypt import java.security.Security -import java.util.* import javax.inject.Inject -import kotlin.math.min class TuskyApplication : Application(), HasAndroidInjector { @Inject @@ -57,10 +52,7 @@ class TuskyApplication : Application(), HasAndroidInjector { lateinit var sharedPreferences: SharedPreferences @Inject - lateinit var db: AppDatabase - - @Inject - lateinit var accountManager: AccountManager + lateinit var occurrenceRespository: OccurrenceRepository override fun onCreate() { // Uncomment me to get StrictMode violation logs @@ -77,25 +69,7 @@ class TuskyApplication : Application(), HasAndroidInjector { val existingUncaughtHandler = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { t, e -> - runBlocking { - var rootCause = e - while (rootCause.cause != null && rootCause != rootCause.cause) { - rootCause = rootCause.cause!! - } - - val traceString = OccurrenceEntity.reduceTrace(rootCause.stackTrace) - var what = e.message - if (what == null && traceString.isNotEmpty()) { - what = traceString.substring(0, min(200, traceString.length)) - } - db.occurrenceDao().insertOrReplace(OccurrenceEntity( - accountId = accountManager.activeAccount?.id, - type = OccurrenceEntity.Type.CRASH, - what = what ?: "CRASH", - startedAt = Calendar.getInstance().time, - callTrace = traceString - )) - } + occurrenceRespository.handleException(e) existingUncaughtHandler?.uncaughtException(t, e) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/LogToDbInterceptor.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/LogToDbInterceptor.kt new file mode 100644 index 0000000000..9ae35c7882 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/LogToDbInterceptor.kt @@ -0,0 +1,44 @@ +/* Copyright Tusky Contributors + * + * 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 . */ + +package com.keylesspalace.tusky.components.occurrence + +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import java.io.IOException + +class LogToDbInterceptor(private val occurrenceRespository: OccurrenceRepository) : Interceptor { + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request: Request = chain.request() + val what = request.method + " " + request.url.toString() + + val entityId = occurrenceRespository.handleApiCallStart(what) + + val response: Response + try { + response = chain.proceed(request) + occurrenceRespository.handleApiCallFinish(entityId, response.code) + } catch (e: Exception) { + // TODO this case is used? If so add its message to the occurrence entity? + occurrenceRespository.handleApiCallFinish(entityId, 600) + + throw e + } + + return response + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt similarity index 86% rename from app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt rename to app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt index 2a402cb66b..c8634b32b6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/OccurrenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt @@ -14,24 +14,26 @@ * see . */ -package com.keylesspalace.tusky +package com.keylesspalace.tusky.components.occurrence import android.content.Context import android.content.Intent import android.graphics.Color import android.os.Build import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ListAdapter import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ActivityOccurrencesBinding import com.keylesspalace.tusky.databinding.ItemOccurrenceBinding -import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AppDatabase -import com.keylesspalace.tusky.db.OccurrenceEntity import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.util.BindingHolder @@ -45,8 +47,6 @@ import kotlinx.coroutines.runBlocking import java.text.DateFormat import javax.inject.Inject -// TODO should all this be in components/occurrence ? - class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { @Inject @@ -57,10 +57,10 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector @Inject - lateinit var db: AppDatabase + lateinit var occurrenceRepository: OccurrenceRepository @Inject - lateinit var accountManager: AccountManager + lateinit var db: AppDatabase // private val viewModel: ListsViewModel by viewModels { viewModelFactory } @@ -105,17 +105,17 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { // TODO well... runBlocking { - accountManager.activeAccount?.let { - binding.swipeRefreshLayout.isRefreshing = true + binding.swipeRefreshLayout.isRefreshing = true - val occurrences = db.occurrenceDao().loadAll(it.id) - adapter.submitList(occurrences) + val occurrences = occurrenceRepository.loadAll() + Log.i("OCA", "Found occurrences "+occurrences.size) - binding.messageView.visible(occurrences.isEmpty()) - binding.occurrenceList.visible(occurrences.isNotEmpty()) + adapter.submitList(occurrences) - binding.swipeRefreshLayout.isRefreshing = false - } + binding.messageView.visible(occurrences.isEmpty()) + binding.occurrenceList.visible(occurrences.isNotEmpty()) + + binding.swipeRefreshLayout.isRefreshing = false } } @@ -139,6 +139,7 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { ListAdapter>(OccurrenceDiffer) { private val dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT) + private var lastAccount: AccountEntity? = null override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { return BindingHolder(ItemOccurrenceBinding.inflate(LayoutInflater.from(parent.context), parent, false)) @@ -203,7 +204,7 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { ) holder.binding.who.text = if (occurrence.accountId != null) { - val account = db.accountDao().get(occurrence.accountId) + val account = getAccount(occurrence.accountId) account?.displayName ?: "" } else { "" @@ -212,7 +213,17 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { holder.binding.trace.visible(occurrence.callTrace.isNotEmpty()) holder.binding.trace.text = occurrence.callTrace // TODO this could/should be normal multi-line - // TODO cache some objects here? For example that account is probably always the same; or different helper objects (locale, number format, ...) + // TODO cache some objects here? For example different helper objects (locale, number format, ...) + } + + private fun getAccount(accountId: Long): AccountEntity? { + if (lastAccount?.id == accountId) { + return lastAccount + } + + lastAccount = db.accountDao().get(accountId) + + return lastAccount } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceEntity.kt similarity index 96% rename from app/src/main/java/com/keylesspalace/tusky/db/OccurrenceEntity.kt rename to app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceEntity.kt index c7674902df..3d7598247c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceEntity.kt @@ -13,11 +13,12 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.db +package com.keylesspalace.tusky.components.occurrence import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.TypeConverters +import com.keylesspalace.tusky.db.Converters import java.util.* @Entity diff --git a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt new file mode 100644 index 0000000000..7c70002cc7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt @@ -0,0 +1,107 @@ +package com.keylesspalace.tusky.components.occurrence + +import android.util.Log +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import kotlinx.coroutines.runBlocking +import java.util.Calendar +import javax.inject.Inject +import kotlin.math.min + +class OccurrenceRepository @Inject constructor(private val db: AppDatabase, private val accountManager: AccountManager) { + private val CLEANUP_INTERVAL = 5 + private val MAXIMUM_ENTRIES = 100 + + private var lastApiCall: OccurrenceEntity? = null + private var apiCallsCounter = 0 + + private val occurrenceDao = db.occurrenceDao() + + fun loadAll(): List { + val occurrences: List + runBlocking { + occurrences = occurrenceDao.loadAll() + } + + return occurrences + } + + fun handleApiCallStart(what: String): Long { + + // TODO The account id here could be wrong (for worker tasks for example) + + val occurrence = OccurrenceEntity( + accountId = accountManager.activeAccount?.id, + type = OccurrenceEntity.Type.APICALL, + what = what, + startedAt = Calendar.getInstance().time, + callTrace = "" +// callTrace = OccurrenceEntity.reduceTrace(Throwable().stackTrace), + ) + // TODO all stack traces here have no hint where they might have originated (always ThreadPool) + // found kotlinx.coroutines.stacktrace.recovery but that should be on by default? + + val entityId: Long + runBlocking { + // TODO runBlocking is the right thing to do here? + entityId = occurrenceDao.insertOrReplace(occurrence) + } + + lastApiCall = occurrence.copy(id = entityId) + + if (++apiCallsCounter % CLEANUP_INTERVAL == 0) { + runBlocking { + occurrenceDao.cleanup(entityId - MAXIMUM_ENTRIES) + } + } + + return entityId + } + + fun handleApiCallFinish(id: Long, responseCode: Int) { + if (lastApiCall == null || lastApiCall!!.id != id) { + // TODO this is an error(?), or just try to fetch it from db again + Log.e(TAG, "Last occurrence entity not found in handleApiCallFinish: " + lastApiCall?.id) + + return + } + + val occurrence = lastApiCall!!.copy( + finishedAt = Calendar.getInstance().time, + code = responseCode, + ) + + runBlocking { + occurrenceDao.insertOrReplace(occurrence) + } + + lastApiCall = null + } + + fun handleException(exception: Throwable) { + var rootCause = exception + while (rootCause.cause != null && rootCause != rootCause.cause) { + rootCause = rootCause.cause!! + } + + val traceString = OccurrenceEntity.reduceTrace(rootCause.stackTrace) + var what = exception.message + if (what == null && traceString.isNotEmpty()) { + what = traceString.substring(0, min(200, traceString.length)) + } + + runBlocking { + occurrenceDao.insertOrReplace(OccurrenceEntity( + accountId = accountManager.activeAccount?.id, + type = OccurrenceEntity.Type.CRASH, + what = what ?: "CRASH", + startedAt = Calendar.getInstance().time, + callTrace = traceString + )) + } + } + + companion object { + private const val TAG = "OccurrenceRepository" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/OccurrencesViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrencesViewModel.kt similarity index 95% rename from app/src/main/java/com/keylesspalace/tusky/viewmodel/OccurrencesViewModel.kt rename to app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrencesViewModel.kt index 9d22f2fdbd..65ff026b42 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/OccurrencesViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrencesViewModel.kt @@ -14,7 +14,7 @@ * see . */ -package com.keylesspalace.tusky.viewmodel +package com.keylesspalace.tusky.components.occurrence import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt index adeb840737..aac21e5bb9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt @@ -42,7 +42,7 @@ import autodispose2.androidx.lifecycle.autoDispose import com.google.android.material.color.MaterialColors import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BuildConfig -import com.keylesspalace.tusky.OccurrenceActivity +import com.keylesspalace.tusky.components.occurrence.OccurrenceActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.appstore.EventHub @@ -80,7 +80,6 @@ import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.io.IOException diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 3990015b77..8bed3b302d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -23,6 +23,7 @@ import com.keylesspalace.tusky.TabDataKt; import com.keylesspalace.tusky.components.conversation.ConversationEntity; +import com.keylesspalace.tusky.components.occurrence.OccurrenceEntity; import java.io.File; diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt index b9de894ee8..6ff693aaa0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -21,6 +21,7 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.keylesspalace.tusky.TabData import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity +import com.keylesspalace.tusky.components.occurrence.OccurrenceEntity import com.keylesspalace.tusky.createTabDataFromId import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Emoji diff --git a/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt index c5e184cd5f..a3bfb72493 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt @@ -19,6 +19,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import com.keylesspalace.tusky.components.occurrence.OccurrenceEntity @Dao interface OccurrenceDao { @@ -28,9 +29,12 @@ interface OccurrenceDao { // @Query("SELECT * FROM OccurrenceEntity WHERE accountId = :accountId ORDER BY id ASC") // fun pagingSource(accountId: Long): PagingSource - @Query("SELECT * FROM OccurrenceEntity WHERE accountId = :accountId ORDER BY startedAt DESC") - suspend fun loadAll(accountId: Long): List + @Query("SELECT * FROM OccurrenceEntity ORDER BY startedAt DESC") + suspend fun loadAll(): List - @Query("DELETE FROM OccurrenceEntity WHERE id = :id") - suspend fun delete(id: Int) +// @Query("DELETE FROM OccurrenceEntity WHERE id = :id") +// suspend fun delete(id: Int) + + @Query("DELETE FROM OccurrenceEntity WHERE id < :maxId") + suspend fun cleanup(maxId: Long) } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt index e20703adcf..6a8e806581 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -21,7 +21,7 @@ import com.keylesspalace.tusky.EditProfileActivity import com.keylesspalace.tusky.LicenseActivity import com.keylesspalace.tusky.ListsActivity import com.keylesspalace.tusky.MainActivity -import com.keylesspalace.tusky.OccurrenceActivity +import com.keylesspalace.tusky.components.occurrence.OccurrenceActivity import com.keylesspalace.tusky.SplashActivity import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.TabPreferenceActivity diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index b782cbabb4..701f78918c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -24,10 +24,10 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.db.AccountManager -import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.json.Rfc3339DateJsonAdapter import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor -import com.keylesspalace.tusky.network.LogToDbInterceptor +import com.keylesspalace.tusky.components.occurrence.LogToDbInterceptor +import com.keylesspalace.tusky.components.occurrence.OccurrenceRepository import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MediaUploadApi import com.keylesspalace.tusky.settings.PrefKeys.HTTP_PROXY_ENABLED @@ -71,7 +71,7 @@ class NetworkModule { accountManager: AccountManager, context: Context, preferences: SharedPreferences, - db: AppDatabase + occurrenceRespository: OccurrenceRepository ): OkHttpClient { val httpProxyEnabled = preferences.getBoolean(HTTP_PROXY_ENABLED, false) val httpServer = preferences.getNonNullString(HTTP_PROXY_SERVER, "") @@ -108,10 +108,7 @@ class NetworkModule { addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) if (BuildConfig.DEBUG) { addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }) -// System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON) -// System.setProperty(STACKTRACE_RECOVERY_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON) - addInterceptor(LogToDbInterceptor(db.occurrenceDao(), accountManager.activeAccount?.id)) - // TODO probably not really the correct location / should be near Api? The account id here could be wrong with multiple reasons. + addInterceptor(LogToDbInterceptor(occurrenceRespository)) } } .build() diff --git a/app/src/main/java/com/keylesspalace/tusky/network/LogToDbInterceptor.kt b/app/src/main/java/com/keylesspalace/tusky/network/LogToDbInterceptor.kt deleted file mode 100644 index 586b180aeb..0000000000 --- a/app/src/main/java/com/keylesspalace/tusky/network/LogToDbInterceptor.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* Copyright Tusky Contributors - * - * 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 . */ - -package com.keylesspalace.tusky.network - -import com.keylesspalace.tusky.db.OccurrenceDao -import com.keylesspalace.tusky.db.OccurrenceEntity -import kotlinx.coroutines.runBlocking -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response -import java.io.IOException -import java.util.Calendar -import java.util.concurrent.TimeUnit - -class LogToDbInterceptor(private val dao: OccurrenceDao, private val accountId: Long?) : Interceptor { - @Throws(IOException::class) - override fun intercept(chain: Interceptor.Chain): Response { - val request: Request = chain.request() - - val startTime = Calendar.getInstance().time - - val what = OccurrenceEntity( - accountId = accountId, - type = OccurrenceEntity.Type.APICALL, - what = request.method + " " + request.url.toString(), - startedAt = startTime, - callTrace = OccurrenceEntity.reduceTrace(Throwable().stackTrace), - ) - // TODO all stack traces here have no hint where they might have originated (always ThreadPool) - // found kotlinx.coroutines.stacktrace.recovery but that should be on by default? - - val entityId: Long - runBlocking { - // TODO runBlocking is the right thing to do here? - entityId = dao.insertOrReplace(what) - } - - val startNs = System.nanoTime() - val response: Response - try { - response = chain.proceed(request) - } catch (e: Exception) { - throw e - } - - val finishTime = Calendar.getInstance().time - - val afterWhat = what.copy( - id = entityId, - finishedAt = finishTime, - code = response.code, - ) - - runBlocking { - dao.insertOrReplace(afterWhat) - } - - - val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) - - val responseBody = response.body!! - val contentLength = responseBody.contentLength() - - -// network.LogToDbInterceptor.intercept:25; network.InstanceSwitchAuthInterceptor.intercept:70; di.NetworkModule$providesHttpClient$$inlined$-addInterceptor$1.intercept:1086 - return response - } -} From 820c2d1ac444cee8c7bd773616eaecbc2ed3ed7d Mon Sep 17 00:00:00 2001 From: Lakoja Date: Fri, 7 Apr 2023 15:45:39 +0200 Subject: [PATCH 06/12] 3516: Move menu entry to main drawer --- .../com/keylesspalace/tusky/MainActivity.kt | 38 ++++++++++++------- .../occurrence/LogToDbInterceptor.kt | 2 +- .../occurrence/OccurrenceActivity.kt | 4 +- .../occurrence/OccurrenceRepository.kt | 21 +++++----- .../components/timeline/TimelineFragment.kt | 12 ------ .../keylesspalace/tusky/db/OccurrenceDao.kt | 3 ++ app/src/main/res/menu/fragment_timeline.xml | 4 -- 7 files changed, 43 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 0d8da3be97..0be3430e93 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -73,6 +73,7 @@ import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.components.notifications.disableAllNotifications import com.keylesspalace.tusky.components.notifications.enablePushNotificationsWithFallback import com.keylesspalace.tusky.components.notifications.showMigrationNoticeIfNecessary +import com.keylesspalace.tusky.components.occurrence.OccurrenceActivity import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity import com.keylesspalace.tusky.components.search.SearchActivity @@ -632,20 +633,31 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } if (BuildConfig.DEBUG) { - // Add a "Developer tools" entry. Code that makes it easier to - // set the app state at runtime belongs here, it will never - // be exposed to users. - binding.mainDrawer.addItems( - DividerDrawerItem(), - secondaryDrawerItem { - nameText = "Developer tools" - isEnabled = true - iconicsIcon = GoogleMaterial.Icon.gmd_developer_mode - onClick = { - buildDeveloperToolsDialog().show() + binding.mainDrawer.apply { + addItems( + DividerDrawerItem(), + secondaryDrawerItem { + nameRes = R.string.action_occurrences + isEnabled = true + iconicsIcon = GoogleMaterial.Icon.gmd_event_note + onClick = { + startActivityWithSlideInAnimation(Intent(context, OccurrenceActivity::class.java)) + } + }, + + // Add a "Developer tools" entry. Code that makes it easier to + // set the app state at runtime belongs here, it will never + // be exposed to users. + secondaryDrawerItem { + nameText = "Developer tools" + isEnabled = true + iconicsIcon = GoogleMaterial.Icon.gmd_developer_mode + onClick = { + buildDeveloperToolsDialog().show() + } } - } - ) + ) + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/LogToDbInterceptor.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/LogToDbInterceptor.kt index 9ae35c7882..2ad20151dd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/LogToDbInterceptor.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/LogToDbInterceptor.kt @@ -34,7 +34,7 @@ class LogToDbInterceptor(private val occurrenceRespository: OccurrenceRepository occurrenceRespository.handleApiCallFinish(entityId, response.code) } catch (e: Exception) { // TODO this case is used? If so add its message to the occurrence entity? - occurrenceRespository.handleApiCallFinish(entityId, 600) + occurrenceRespository.handleApiCallFinish(entityId, 499) throw e } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt index c8634b32b6..52484573be 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt @@ -195,8 +195,10 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { holder.binding.duration.text = duration holder.binding.duration.setTextColor( if (durationMs >= 1000) { - Color.RED + Color.MAGENTA } else if (durationMs >= 400) { + // TODO these colors are a bit of a problem: e. g. on light mode yellow is hardly visible and green is difficult + Color.YELLOW } else { Color.GREEN diff --git a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt index 7c70002cc7..87494843a0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt @@ -9,10 +9,7 @@ import javax.inject.Inject import kotlin.math.min class OccurrenceRepository @Inject constructor(private val db: AppDatabase, private val accountManager: AccountManager) { - private val CLEANUP_INTERVAL = 5 - private val MAXIMUM_ENTRIES = 100 - - private var lastApiCall: OccurrenceEntity? = null + private var lastApiCalls = HashMap(13) private var apiCallsCounter = 0 private val occurrenceDao = db.occurrenceDao() @@ -47,7 +44,7 @@ class OccurrenceRepository @Inject constructor(private val db: AppDatabase, priv entityId = occurrenceDao.insertOrReplace(occurrence) } - lastApiCall = occurrence.copy(id = entityId) + lastApiCalls[entityId] = occurrence.copy(id = entityId) if (++apiCallsCounter % CLEANUP_INTERVAL == 0) { runBlocking { @@ -59,14 +56,15 @@ class OccurrenceRepository @Inject constructor(private val db: AppDatabase, priv } fun handleApiCallFinish(id: Long, responseCode: Int) { - if (lastApiCall == null || lastApiCall!!.id != id) { - // TODO this is an error(?), or just try to fetch it from db again - Log.e(TAG, "Last occurrence entity not found in handleApiCallFinish: " + lastApiCall?.id) + val startedOccurrence = lastApiCalls[id] + + if (startedOccurrence == null) { + Log.e(TAG, "Last occurrence entity not found in handleApiCallFinish for $id") return } - val occurrence = lastApiCall!!.copy( + val occurrence = startedOccurrence.copy( finishedAt = Calendar.getInstance().time, code = responseCode, ) @@ -75,7 +73,8 @@ class OccurrenceRepository @Inject constructor(private val db: AppDatabase, priv occurrenceDao.insertOrReplace(occurrence) } - lastApiCall = null + lastApiCalls.remove(id) + // TODO that map can grow (lots of unfinished calls that are never removed)? } fun handleException(exception: Throwable) { @@ -103,5 +102,7 @@ class OccurrenceRepository @Inject constructor(private val db: AppDatabase, priv companion object { private const val TAG = "OccurrenceRepository" + private const val CLEANUP_INTERVAL = 5 + private const val MAXIMUM_ENTRIES = 100 } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt index aac21e5bb9..f58e16854f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt @@ -41,8 +41,6 @@ import at.connyduck.sparkbutton.helpers.Utils import autodispose2.androidx.lifecycle.autoDispose import com.google.android.material.color.MaterialColors import com.keylesspalace.tusky.BaseActivity -import com.keylesspalace.tusky.BuildConfig -import com.keylesspalace.tusky.components.occurrence.OccurrenceActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.appstore.EventHub @@ -337,10 +335,6 @@ class TimelineFragment : MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary) } } - - if (!BuildConfig.DEBUG) { - menu.removeItem(R.id.action_occurrences) - } } } @@ -356,12 +350,6 @@ class TimelineFragment : false } } - R.id.action_occurrences -> { - // TODO should/could be placed in main drawer? - startActivity(Intent(this.context, OccurrenceActivity::class.java)) - - true - } else -> false } } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt index a3bfb72493..c50c978287 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/OccurrenceDao.kt @@ -23,6 +23,9 @@ import com.keylesspalace.tusky.components.occurrence.OccurrenceEntity @Dao interface OccurrenceDao { + @Query("SELECT * FROM OccurrenceEntity WHERE id = :id LIMIT 1") + fun get(id: Long): OccurrenceEntity? + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertOrReplace(one: OccurrenceEntity): Long diff --git a/app/src/main/res/menu/fragment_timeline.xml b/app/src/main/res/menu/fragment_timeline.xml index f90dbb8f2b..bf722917fe 100644 --- a/app/src/main/res/menu/fragment_timeline.xml +++ b/app/src/main/res/menu/fragment_timeline.xml @@ -5,8 +5,4 @@ android:id="@+id/action_refresh" android:title="@string/action_refresh" app:showAsAction="never" /> - From 68f3322f37566bd189a919f9176e56780da39b9c Mon Sep 17 00:00:00 2001 From: Lakoja Date: Sat, 8 Apr 2023 23:37:04 +0200 Subject: [PATCH 07/12] 3516: Use constraint layout --- app/src/main/res/layout/item_occurrence.xml | 98 ++++++++++++--------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/app/src/main/res/layout/item_occurrence.xml b/app/src/main/res/layout/item_occurrence.xml index d04c293bd3..3189a7a6a2 100644 --- a/app/src/main/res/layout/item_occurrence.xml +++ b/app/src/main/res/layout/item_occurrence.xml @@ -1,71 +1,81 @@ - - - - + android:lines="1" + android:minWidth="50dp" + android:paddingEnd="6dp" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + app:layout_constraintEnd_toStartOf="@id/whenDate" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/what" + tools:text="500" /> - + - + - - + - + app:layout_constraintTop_toBottomOf="@+id/code" + tools:text="Trace line 1" + tools:visibility="visible" /> + From 78924692701c9abfaab9a1937501a81535dfc743 Mon Sep 17 00:00:00 2001 From: Lakoja Date: Mon, 10 Apr 2023 09:45:13 +0200 Subject: [PATCH 08/12] 3516: Refresh async --- .../occurrence/OccurrenceActivity.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt index 52484573be..a6e09330bb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt @@ -21,9 +21,9 @@ import android.content.Intent import android.graphics.Color import android.os.Build import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ListAdapter @@ -43,7 +43,7 @@ import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.launch import java.text.DateFormat import javax.inject.Inject @@ -52,7 +52,6 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { @Inject lateinit var viewModelFactory: ViewModelFactory - // TODO what's this? @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector @@ -68,6 +67,8 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { private val adapter = OccurrenceAdapter() + private var loading = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -99,16 +100,15 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { } private fun load() { -// if (binding.swipeRefreshLayout.isRefreshing) { -// return -// } + if (loading) { + return + } - // TODO well... - runBlocking { + lifecycleScope.launch { binding.swipeRefreshLayout.isRefreshing = true + loading = true val occurrences = occurrenceRepository.loadAll() - Log.i("OCA", "Found occurrences "+occurrences.size) adapter.submitList(occurrences) @@ -116,6 +116,7 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { binding.occurrenceList.visible(occurrences.isNotEmpty()) binding.swipeRefreshLayout.isRefreshing = false + loading = false } } From 67e385aaef709b7427947fe6b14fb6c70544a3a0 Mon Sep 17 00:00:00 2001 From: Lakoja Date: Mon, 10 Apr 2023 09:59:57 +0200 Subject: [PATCH 09/12] 3516: Use themable colors --- .../components/occurrence/OccurrenceActivity.kt | 14 ++++++-------- app/src/main/res/values-night/theme_colors.xml | 5 +++++ app/src/main/res/values/colors.xml | 2 ++ app/src/main/res/values/theme_colors.xml | 5 +++++ 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt index a6e09330bb..de6451c6d7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt @@ -164,11 +164,11 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { holder.binding.code.setTextColor( if (occurrence.code != null && occurrence.code > 0) { if (occurrence.code >= 400) { - Color.RED + baseContext.getColor(R.color.colorError) } else if (occurrence.code >= 300) { - Color.YELLOW + baseContext.getColor(R.color.colorWarning) } else { - Color.GREEN + baseContext.getColor(R.color.colorSuccess) } } else { defaultTextColor @@ -196,13 +196,11 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { holder.binding.duration.text = duration holder.binding.duration.setTextColor( if (durationMs >= 1000) { - Color.MAGENTA + baseContext.getColor(R.color.colorBad) } else if (durationMs >= 400) { - // TODO these colors are a bit of a problem: e. g. on light mode yellow is hardly visible and green is difficult - - Color.YELLOW + baseContext.getColor(R.color.colorWarning) } else { - Color.GREEN + baseContext.getColor(R.color.colorSuccess) } ) diff --git a/app/src/main/res/values-night/theme_colors.xml b/app/src/main/res/values-night/theme_colors.xml index 57f15799b3..a5db33dbca 100644 --- a/app/src/main/res/values-night/theme_colors.xml +++ b/app/src/main/res/values-night/theme_colors.xml @@ -31,4 +31,9 @@ #00731B #DF0000 + + @color/tusky_green + @color/tusky_orange_light + @color/tusky_magenta + @color/tusky_red diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 79a16f803c..6fd9580e9f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -6,6 +6,8 @@ #fab207 #19a341 #25d069 + #148033 + #df15c1 #DF1553 #fff diff --git a/app/src/main/res/values/theme_colors.xml b/app/src/main/res/values/theme_colors.xml index 32f2727fd0..d0908ee011 100644 --- a/app/src/main/res/values/theme_colors.xml +++ b/app/src/main/res/values/theme_colors.xml @@ -31,4 +31,9 @@ #CCFFD8 #FFC0C0 + + @color/tusky_green_dark + @color/tusky_orange + @color/tusky_magenta + @color/tusky_red From 2b2c7057a4a3b855059228bb0ea75e5589674355 Mon Sep 17 00:00:00 2001 From: Lakoja Date: Mon, 10 Apr 2023 10:10:18 +0200 Subject: [PATCH 10/12] 3516: (collateral) Use consistent colors --- app/src/main/res/values-night/theme_colors.xml | 5 ++--- app/src/main/res/values/colors.xml | 3 ++- app/src/main/res/values/theme_colors.xml | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/values-night/theme_colors.xml b/app/src/main/res/values-night/theme_colors.xml index a5db33dbca..66e772884b 100644 --- a/app/src/main/res/values-night/theme_colors.xml +++ b/app/src/main/res/values-night/theme_colors.xml @@ -28,9 +28,8 @@ @color/white @color/tusky_grey_10 - - #00731B - #DF0000 + @color/tusky_green + @color/tusky_red @color/tusky_green @color/tusky_orange_light diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6fd9580e9f..91cabe795e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -8,7 +8,8 @@ #25d069 #148033 #df15c1 - #DF1553 + #df1553 + #ed3b70 #fff #000 diff --git a/app/src/main/res/values/theme_colors.xml b/app/src/main/res/values/theme_colors.xml index d0908ee011..dea4a75598 100644 --- a/app/src/main/res/values/theme_colors.xml +++ b/app/src/main/res/values/theme_colors.xml @@ -28,9 +28,8 @@ @color/tusky_grey_20 @color/white - - #CCFFD8 - #FFC0C0 + @color/tusky_green + @color/tusky_red_light @color/tusky_green_dark @color/tusky_orange From 711d85a2900ce465a45b2a84a4bc00a886daee7f Mon Sep 17 00:00:00 2001 From: Lakoja Date: Sun, 16 Apr 2023 11:40:12 +0200 Subject: [PATCH 11/12] 3516: Add comments about log interception --- .../java/com/keylesspalace/tusky/MainActivity.kt | 13 +++++++++++++ .../components/occurrence/OccurrenceRepository.kt | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 0be3430e93..556826c28b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -184,6 +184,19 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje val activeAccount = accountManager.activeAccount ?: return // will be redirected to LoginActivity by BaseActivity + // TODO this works but seems a bit blunt for "intercept relevant log messages"? +// lifecycleScope.launch { +// Runtime.getRuntime().exec("logcat -c") +// Runtime.getRuntime().exec("logcat") +// .inputStream +// .bufferedReader() +// .useLines { lines -> lines.forEach { +// val x = it +// val y = 0 +// } +// } +// } + var showNotificationTab = false if (intent != null) { /** there are two possibilities the accountId can be passed to MainActivity: diff --git a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt index 87494843a0..75469431cf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt @@ -23,6 +23,9 @@ class OccurrenceRepository @Inject constructor(private val db: AppDatabase, priv return occurrences } + // TODO this could/should also record warning and error logs (from "Log"). + // However that seems not to be intercept-able? Also see commented code block in MainActivity.onCreate + fun handleApiCallStart(what: String): Long { // TODO The account id here could be wrong (for worker tasks for example) @@ -37,6 +40,7 @@ class OccurrenceRepository @Inject constructor(private val db: AppDatabase, priv ) // TODO all stack traces here have no hint where they might have originated (always ThreadPool) // found kotlinx.coroutines.stacktrace.recovery but that should be on by default? + // There is also a kotlinx.coroutines.debug.DebugProbes. But that hangs on "install()". val entityId: Long runBlocking { From e5e529b5729c63f5894380972087f7ed795af016 Mon Sep 17 00:00:00 2001 From: Lakoja Date: Sun, 16 Apr 2023 12:28:44 +0200 Subject: [PATCH 12/12] 3516: Save full stack, only reduce on display --- .../com.keylesspalace.tusky.db.AppDatabase/49.json | 4 ++-- .../tusky/components/occurrence/OccurrenceActivity.kt | 2 +- .../tusky/components/occurrence/OccurrenceEntity.kt | 3 +-- .../components/occurrence/OccurrenceRepository.kt | 6 +++--- .../main/java/com/keylesspalace/tusky/db/Converters.kt | 10 ++++++++++ 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/49.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/49.json index 3e0a26c2ce..622da448a3 100644 --- a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/49.json +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/49.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 49, - "identityHash": "e5748dfe3c822a1305530005ac41f534", + "identityHash": "90c888ce17bbc144b95208b6ecc4e10a", "entities": [ { "tableName": "DraftEntity", @@ -1054,4 +1054,4 @@ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e5748dfe3c822a1305530005ac41f534')" ] } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt index de6451c6d7..577f932f8d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceActivity.kt @@ -212,7 +212,7 @@ class OccurrenceActivity : BaseActivity(), Injectable, HasAndroidInjector { } holder.binding.trace.visible(occurrence.callTrace.isNotEmpty()) - holder.binding.trace.text = occurrence.callTrace // TODO this could/should be normal multi-line + holder.binding.trace.text = OccurrenceEntity.reduceTrace(occurrence.callTrace) // TODO cache some objects here? For example different helper objects (locale, number format, ...) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceEntity.kt index 3d7598247c..0908fe7628 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceEntity.kt @@ -31,12 +31,11 @@ data class OccurrenceEntity( val startedAt: Date, // TODO or use LocalDateTime (or Long)? val finishedAt: Date? = null, val code: Int? = null, - val callTrace: String, + val callTrace: Array, ) { companion object { fun reduceTrace(stackTrace: Array): String { // TODO conditions/transforms here are a bit arbitrary... - // TODO we could maybe record more (full stack?) in the db and only reduce it for display? // TODO probably keep at least the last non-Tusky location in the stack; and/or keep the information that some entries were removed var tuskyTrace = stackTrace.filter { it.className.startsWith("com.keylesspalace.tusky") && !it.methodName.contains("intercept") } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt index 75469431cf..76466b29fe 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/occurrence/OccurrenceRepository.kt @@ -35,8 +35,8 @@ class OccurrenceRepository @Inject constructor(private val db: AppDatabase, priv type = OccurrenceEntity.Type.APICALL, what = what, startedAt = Calendar.getInstance().time, - callTrace = "" -// callTrace = OccurrenceEntity.reduceTrace(Throwable().stackTrace), + callTrace = emptyArray() +// callTrace = Throwable().stackTrace, ) // TODO all stack traces here have no hint where they might have originated (always ThreadPool) // found kotlinx.coroutines.stacktrace.recovery but that should be on by default? @@ -99,7 +99,7 @@ class OccurrenceRepository @Inject constructor(private val db: AppDatabase, priv type = OccurrenceEntity.Type.CRASH, what = what ?: "CRASH", startedAt = Calendar.getInstance().time, - callTrace = traceString + callTrace = rootCause.stackTrace )) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt index 6ff693aaa0..7a5202703f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -187,4 +187,14 @@ class Converters @Inject constructor( fun stringToOccurrenceType(type: String): OccurrenceEntity.Type { return OccurrenceEntity.Type.fromString(type) } + + @TypeConverter + fun stackTraceToJson(stackTrace: Array): String { + return gson.toJson(stackTrace) + } + + @TypeConverter + fun jsonToStackTrace(stackTraceJson: String): Array { + return gson.fromJson(stackTraceJson, object : TypeToken>() {}.type) + } }