Move Exodus trackers to a local database

Replace the bundled exodus_trackers.json asset with a Room-backed tracker
dictionary (exodus_tracker table) synced from the Exodus /api/trackers endpoint
every 3 days via a WorkManager job enqueued after session verification on the
splash screen. Per-app reports are still fetched live from the search API and
their tracker ids resolved to details from the local table (best-effort). Also
fixes Exodus report deserialization to read the snake_case version_code and
creation_date keys.
This commit is contained in:
Rahul Patel
2026-07-24 06:35:33 +05:30
parent d206634936
commit c5038d455e
17 changed files with 1474 additions and 5691 deletions

View File

@@ -0,0 +1,560 @@
{
"formatVersion": 1,
"database": {
"version": 11,
"identityHash": "b0023e89b17406affdf61fc021efb2ad",
"entities": [
{
"tableName": "download",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `offerType` INTEGER NOT NULL, `isInstalled` INTEGER NOT NULL, `displayName` TEXT NOT NULL, `iconURL` TEXT NOT NULL, `size` INTEGER NOT NULL, `id` INTEGER NOT NULL, `downloadStatus` TEXT NOT NULL, `progress` INTEGER NOT NULL, `speed` INTEGER NOT NULL, `timeRemaining` INTEGER NOT NULL, `totalFiles` INTEGER NOT NULL, `downloadedFiles` INTEGER NOT NULL, `fileList` TEXT NOT NULL, `sharedLibs` TEXT NOT NULL, `targetSdk` INTEGER NOT NULL, `downloadedAt` INTEGER NOT NULL, `requiresGMS` INTEGER NOT NULL, PRIMARY KEY(`packageName`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "versionCode",
"columnName": "versionCode",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "offerType",
"columnName": "offerType",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isInstalled",
"columnName": "isInstalled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "iconURL",
"columnName": "iconURL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "downloadStatus",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "speed",
"columnName": "speed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timeRemaining",
"columnName": "timeRemaining",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "totalFiles",
"columnName": "totalFiles",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "downloadedFiles",
"columnName": "downloadedFiles",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fileList",
"columnName": "fileList",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sharedLibs",
"columnName": "sharedLibs",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "targetSdk",
"columnName": "targetSdk",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "downloadedAt",
"columnName": "downloadedAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "requiresGMS",
"columnName": "requiresGMS",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName"
]
}
},
{
"tableName": "favourite",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `displayName` TEXT NOT NULL, `iconURL` TEXT NOT NULL, `added` INTEGER NOT NULL, `mode` TEXT NOT NULL, PRIMARY KEY(`packageName`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "iconURL",
"columnName": "iconURL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "added",
"columnName": "added",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mode",
"columnName": "mode",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName"
]
}
},
{
"tableName": "update",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `versionName` TEXT NOT NULL, `displayName` TEXT NOT NULL, `iconURL` TEXT NOT NULL, `changelog` TEXT NOT NULL, `id` INTEGER NOT NULL, `developerName` TEXT NOT NULL, `size` INTEGER NOT NULL, `updatedOn` TEXT NOT NULL, `hasValidCert` INTEGER NOT NULL, `offerType` INTEGER NOT NULL, `fileList` TEXT NOT NULL, `sharedLibs` TEXT NOT NULL, `targetSdk` INTEGER NOT NULL, `isIncompatible` INTEGER NOT NULL, PRIMARY KEY(`packageName`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "versionCode",
"columnName": "versionCode",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionName",
"columnName": "versionName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "iconURL",
"columnName": "iconURL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "changelog",
"columnName": "changelog",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "developerName",
"columnName": "developerName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updatedOn",
"columnName": "updatedOn",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hasValidCert",
"columnName": "hasValidCert",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "offerType",
"columnName": "offerType",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fileList",
"columnName": "fileList",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sharedLibs",
"columnName": "sharedLibs",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "targetSdk",
"columnName": "targetSdk",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isIncompatible",
"columnName": "isIncompatible",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName"
]
}
},
{
"tableName": "ignored_update",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `ignoredVersionCode` INTEGER, PRIMARY KEY(`packageName`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ignoredVersionCode",
"columnName": "ignoredVersionCode",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName"
]
}
},
{
"tableName": "review",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `accountEmail` TEXT NOT NULL, `title` TEXT NOT NULL, `comment` TEXT NOT NULL, `rating` INTEGER NOT NULL, `commentId` TEXT NOT NULL, `userName` TEXT NOT NULL, `userPhotoUrl` TEXT NOT NULL, `appVersion` TEXT NOT NULL, `timeStamp` INTEGER NOT NULL, `synced` INTEGER NOT NULL, PRIMARY KEY(`packageName`, `accountEmail`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountEmail",
"columnName": "accountEmail",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "comment",
"columnName": "comment",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "rating",
"columnName": "rating",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "commentId",
"columnName": "commentId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userName",
"columnName": "userName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userPhotoUrl",
"columnName": "userPhotoUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "appVersion",
"columnName": "appVersion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timeStamp",
"columnName": "timeStamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synced",
"columnName": "synced",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName",
"accountEmail"
]
}
},
{
"tableName": "account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `profilePicUrl` TEXT, `aasToken` TEXT, `authToken` TEXT, `tokenType` TEXT NOT NULL, `authViaMicroG` INTEGER NOT NULL, `authDataJson` TEXT, `isDefault` INTEGER NOT NULL, `addedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT"
},
{
"fieldPath": "profilePicUrl",
"columnName": "profilePicUrl",
"affinity": "TEXT"
},
{
"fieldPath": "aasToken",
"columnName": "aasToken",
"affinity": "TEXT"
},
{
"fieldPath": "authToken",
"columnName": "authToken",
"affinity": "TEXT"
},
{
"fieldPath": "tokenType",
"columnName": "tokenType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "authViaMicroG",
"columnName": "authViaMicroG",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authDataJson",
"columnName": "authDataJson",
"affinity": "TEXT"
},
{
"fieldPath": "isDefault",
"columnName": "isDefault",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "addedAt",
"columnName": "addedAt",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "app_account_binding",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `accountId` TEXT NOT NULL, PRIMARY KEY(`packageName`), FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName"
]
},
"indices": [
{
"name": "index_app_account_binding_accountId",
"unique": false,
"columnNames": [
"accountId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_account_binding_accountId` ON `${TABLE_NAME}` (`accountId`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "exodus_tracker",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `signature` TEXT NOT NULL, `date` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "signature",
"columnName": "signature",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"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, 'b0023e89b17406affdf61fc021efb2ad')"
]
}
}

View File

@@ -0,0 +1,566 @@
{
"formatVersion": 1,
"database": {
"version": 12,
"identityHash": "79baa70617146871e27d9a37a59f1814",
"entities": [
{
"tableName": "download",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `offerType` INTEGER NOT NULL, `isInstalled` INTEGER NOT NULL, `displayName` TEXT NOT NULL, `iconURL` TEXT NOT NULL, `size` INTEGER NOT NULL, `id` INTEGER NOT NULL, `downloadStatus` TEXT NOT NULL, `progress` INTEGER NOT NULL, `speed` INTEGER NOT NULL, `timeRemaining` INTEGER NOT NULL, `totalFiles` INTEGER NOT NULL, `downloadedFiles` INTEGER NOT NULL, `fileList` TEXT NOT NULL, `sharedLibs` TEXT NOT NULL, `targetSdk` INTEGER NOT NULL, `downloadedAt` INTEGER NOT NULL, `requiresGMS` INTEGER NOT NULL, PRIMARY KEY(`packageName`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "versionCode",
"columnName": "versionCode",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "offerType",
"columnName": "offerType",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isInstalled",
"columnName": "isInstalled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "iconURL",
"columnName": "iconURL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "downloadStatus",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "speed",
"columnName": "speed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timeRemaining",
"columnName": "timeRemaining",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "totalFiles",
"columnName": "totalFiles",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "downloadedFiles",
"columnName": "downloadedFiles",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fileList",
"columnName": "fileList",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sharedLibs",
"columnName": "sharedLibs",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "targetSdk",
"columnName": "targetSdk",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "downloadedAt",
"columnName": "downloadedAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "requiresGMS",
"columnName": "requiresGMS",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName"
]
}
},
{
"tableName": "favourite",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `displayName` TEXT NOT NULL, `iconURL` TEXT NOT NULL, `added` INTEGER NOT NULL, `mode` TEXT NOT NULL, PRIMARY KEY(`packageName`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "iconURL",
"columnName": "iconURL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "added",
"columnName": "added",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mode",
"columnName": "mode",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName"
]
}
},
{
"tableName": "update",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `versionName` TEXT NOT NULL, `displayName` TEXT NOT NULL, `iconURL` TEXT NOT NULL, `changelog` TEXT NOT NULL, `id` INTEGER NOT NULL, `developerName` TEXT NOT NULL, `size` INTEGER NOT NULL, `updatedOn` TEXT NOT NULL, `hasValidCert` INTEGER NOT NULL, `offerType` INTEGER NOT NULL, `fileList` TEXT NOT NULL, `sharedLibs` TEXT NOT NULL, `targetSdk` INTEGER NOT NULL, `isIncompatible` INTEGER NOT NULL, PRIMARY KEY(`packageName`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "versionCode",
"columnName": "versionCode",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "versionName",
"columnName": "versionName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "iconURL",
"columnName": "iconURL",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "changelog",
"columnName": "changelog",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "developerName",
"columnName": "developerName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updatedOn",
"columnName": "updatedOn",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "hasValidCert",
"columnName": "hasValidCert",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "offerType",
"columnName": "offerType",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fileList",
"columnName": "fileList",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sharedLibs",
"columnName": "sharedLibs",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "targetSdk",
"columnName": "targetSdk",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isIncompatible",
"columnName": "isIncompatible",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName"
]
}
},
{
"tableName": "ignored_update",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `ignoredVersionCode` INTEGER, PRIMARY KEY(`packageName`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ignoredVersionCode",
"columnName": "ignoredVersionCode",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName"
]
}
},
{
"tableName": "review",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `accountEmail` TEXT NOT NULL, `title` TEXT NOT NULL, `comment` TEXT NOT NULL, `rating` INTEGER NOT NULL, `commentId` TEXT NOT NULL, `userName` TEXT NOT NULL, `userPhotoUrl` TEXT NOT NULL, `appVersion` TEXT NOT NULL, `timeStamp` INTEGER NOT NULL, `synced` INTEGER NOT NULL, PRIMARY KEY(`packageName`, `accountEmail`))",
"fields": [
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountEmail",
"columnName": "accountEmail",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "comment",
"columnName": "comment",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "rating",
"columnName": "rating",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "commentId",
"columnName": "commentId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userName",
"columnName": "userName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userPhotoUrl",
"columnName": "userPhotoUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "appVersion",
"columnName": "appVersion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timeStamp",
"columnName": "timeStamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synced",
"columnName": "synced",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName",
"accountEmail"
]
}
},
{
"tableName": "account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `profilePicUrl` TEXT, `aasToken` TEXT, `authToken` TEXT, `tokenType` TEXT NOT NULL, `authViaMicroG` INTEGER NOT NULL, `authDataJson` TEXT, `isDefault` INTEGER NOT NULL, `addedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT"
},
{
"fieldPath": "profilePicUrl",
"columnName": "profilePicUrl",
"affinity": "TEXT"
},
{
"fieldPath": "aasToken",
"columnName": "aasToken",
"affinity": "TEXT"
},
{
"fieldPath": "authToken",
"columnName": "authToken",
"affinity": "TEXT"
},
{
"fieldPath": "tokenType",
"columnName": "tokenType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "authViaMicroG",
"columnName": "authViaMicroG",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authDataJson",
"columnName": "authDataJson",
"affinity": "TEXT"
},
{
"fieldPath": "isDefault",
"columnName": "isDefault",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "addedAt",
"columnName": "addedAt",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "app_account_binding",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `accountId` TEXT NOT NULL, PRIMARY KEY(`packageName`), FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "packageName",
"columnName": "packageName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"packageName"
]
},
"indices": [
{
"name": "index_app_account_binding_accountId",
"unique": false,
"columnNames": [
"accountId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_app_account_binding_accountId` ON `${TABLE_NAME}` (`accountId`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"accountId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "exodus_tracker",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `signature` TEXT NOT NULL, `date` TEXT NOT NULL, `categories` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "signature",
"columnName": "signature",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "categories",
"columnName": "categories",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"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, '79baa70617146871e27d9a37a59f1814')"
]
}
}

View File

@@ -1,50 +0,0 @@
package com.aurora.store.data.providers
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import javax.inject.Inject
import org.json.JSONObject
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class ExodusModuleTest {
@get:Rule
var hiltAndroidRule = HiltAndroidRule(this)
@Inject
lateinit var exodusTrackers: JSONObject
@Before
fun setup() {
hiltAndroidRule.inject()
}
@Test
fun testTrackersJsonIsNotEmpty() {
assertThat(exodusTrackers.toString()).isNotEmpty()
}
@Test
fun testTrackersJsonContainsTrackers() {
val trackers = mapOf(
"com.facebook.flipper" to 392,
"com.google.analytics." to 48,
"com.google.firebase.firebase_analytics" to 49,
"com.google.ads." to 312
)
trackers.forEach { (codeSignature, id) ->
assertThat(
exodusTrackers.getJSONObject(id.toString())
.getString("code_signature")
.contains(codeSignature)
).isTrue()
}
}
}

View File

File diff suppressed because one or more lines are too long

View File

@@ -31,6 +31,7 @@ object Constants {
const val EXODUS_SUBMIT_PAGE = "https://reports.exodus-privacy.eu.org/analysis/submit/#"
const val EXODUS_REPORT_URL = "https://reports.exodus-privacy.eu.org/reports/"
const val EXODUS_SEARCH_URL = "https://reports.exodus-privacy.eu.org/api/search/"
const val EXODUS_TRACKERS_URL = "https://reports.exodus-privacy.eu.org/api/trackers"
const val PLEXUS_API_URL = "https://plexus.techlore.tech/api/v1/apps"
const val PLEXUS_SEARCH_URL = "https://plexus.techlore.tech/?q="

View File

@@ -63,6 +63,7 @@ import com.aurora.store.compose.composition.LocalNetworkStatus
import com.aurora.store.compose.navigation.Destination
import com.aurora.store.data.model.AuthState
import com.aurora.store.data.model.NetworkStatus
import com.aurora.store.data.work.ExodusTrackerWorker
import com.aurora.store.util.CertUtil.GOOGLE_ACCOUNT_TYPE
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_AUTH_TOKEN_TYPE
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT
@@ -113,6 +114,7 @@ fun SplashScreen(
AuthState.Valid, AuthState.SignedIn -> {
anonymousLoading = false
googleLoading = false
ExodusTrackerWorker.enqueue(context)
when {
!deepLinkDevId.isNullOrBlank() -> onNavigateTo(
Destination.DevProfile(deepLinkDevId)

View File

@@ -0,0 +1,137 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.data
import android.content.Context
import android.util.Log
import com.aurora.Constants
import com.aurora.extensions.TAG
import com.aurora.gplayapi.network.IHttpClient
import com.aurora.store.BuildConfig
import com.aurora.store.data.model.ExodusReport
import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.data.model.Report
import com.aurora.store.data.model.TrackersResponse
import com.aurora.store.data.room.exodus.TrackerDao
import com.aurora.store.data.room.exodus.TrackerEntity
import com.aurora.store.util.Preferences
import com.aurora.store.util.Preferences.PREFERENCE_LAST_TRACKER_SYNC
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import org.json.JSONObject
@Singleton
class ExodusRepository @Inject constructor(
private val httpClient: IHttpClient,
private val json: Json,
private val trackerDao: TrackerDao,
@ApplicationContext private val context: Context
) {
companion object {
private val SYNC_INTERVAL_MS = TimeUnit.DAYS.toMillis(3)
private const val CATEGORY_SEPARATOR = "\n"
}
private val authHeaders: Map<String, String>
get() = mapOf(
"Content-Type" to Constants.JSON_MIME_TYPE,
"Accept" to Constants.JSON_MIME_TYPE,
"Authorization" to "Token ${BuildConfig.EXODUS_API_KEY}"
)
suspend fun fetchReports(packageName: String): List<Report> = withContext(Dispatchers.IO) {
try {
val response = httpClient.get("${Constants.EXODUS_SEARCH_URL}$packageName", authHeaders)
parseReports(String(response.responseBytes), packageName)
} catch (exception: Exception) {
Log.e(TAG, "Failed to fetch exodus reports", exception)
emptyList()
}
}
private fun parseReports(response: String, packageName: String): List<Report> = try {
val exodusObject = JSONObject(response).getJSONObject(packageName)
json.decodeFromString<ExodusReport>(exodusObject.toString()).reports
} catch (_: Exception) {
emptyList()
}
suspend fun syncTrackersIfStale() = withContext(Dispatchers.IO) {
val lastSync = Preferences.getLong(context, PREFERENCE_LAST_TRACKER_SYNC)
val isStale = System.currentTimeMillis() - lastSync > SYNC_INTERVAL_MS
if (trackerDao.count() > 0 && !isStale) return@withContext
val response = httpClient.get(Constants.EXODUS_TRACKERS_URL, authHeaders)
val trackers = json.decodeFromString<TrackersResponse>(String(response.responseBytes))
.trackers.values.map {
TrackerEntity(
id = it.id,
name = it.name,
url = it.website,
signature = it.codeSignature,
date = it.creationDate,
categories = it.categories.joinToString(CATEGORY_SEPARATOR)
)
}
if (trackers.isNotEmpty()) {
trackerDao.purgeAndInsert(trackers)
Preferences.putLong(context, PREFERENCE_LAST_TRACKER_SYNC, System.currentTimeMillis())
}
}
suspend fun resolveTrackers(ids: List<Int>): List<ExodusTracker> {
if (ids.isEmpty()) return emptyList()
val byId = trackerDao.getByIds(ids).associateBy { it.id }
return ids.map { id ->
byId[id]?.let {
ExodusTracker(
id = it.id,
name = it.name,
url = it.url,
signature = it.signature,
date = it.date,
categories = if (it.categories.isBlank()) {
emptyList()
} else {
it.categories.split(CATEGORY_SEPARATOR)
}
)
} ?: ExodusTracker(id = id, name = "Tracker #$id")
}
}
suspend fun getNewTrackers(
packageName: String,
installedVersionCode: Long
): List<ExodusTracker> = computeNewTrackers(fetchReports(packageName), installedVersionCode)
suspend fun computeNewTrackers(
reports: List<Report>,
installedVersionCode: Long
): List<ExodusTracker> {
if (reports.isEmpty()) return emptyList()
val installedReport = reports.firstOrNull {
it.versionCode.toLongOrNull() == installedVersionCode
} ?: return emptyList()
val newestReport = reports.maxByOrNull { it.versionCode.toLongOrNull() ?: -1L }
?: return emptyList()
val newestVersionCode = newestReport.versionCode.toLongOrNull() ?: return emptyList()
if (newestVersionCode <= installedVersionCode) return emptyList()
val newIds = newestReport.trackers.toSet() - installedReport.trackers.toSet()
return resolveTrackers(newIds.toList())
}
}

View File

@@ -21,6 +21,7 @@ package com.aurora.store.data.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@@ -36,9 +37,9 @@ data class Report(
val id: Int = -1,
val downloads: String = String(),
val version: String = String(),
val creationDate: String = String(),
val updatedAt: String = String(),
val versionCode: String = String(),
@SerialName("creation_date") val creationDate: String = String(),
@SerialName("updated_at") val updatedAt: String = String(),
@SerialName("version_code") val versionCode: String = String(),
val trackers: List<Int> = listOf()
) : Parcelable
@@ -62,3 +63,18 @@ data class ExodusTracker(
else -> false
}
}
@Serializable
data class TrackersResponse(
val trackers: Map<String, TrackerDto> = emptyMap()
)
@Serializable
data class TrackerDto(
val id: Int = 0,
val name: String = String(),
@SerialName("website") val website: String = String(),
@SerialName("code_signature") val codeSignature: String = String(),
@SerialName("creation_date") val creationDate: String = String(),
val categories: List<String> = emptyList()
)

View File

@@ -11,6 +11,8 @@ import com.aurora.store.data.room.account.AppAccountBindingDao
import com.aurora.store.data.room.download.Download
import com.aurora.store.data.room.download.DownloadConverter
import com.aurora.store.data.room.download.DownloadDao
import com.aurora.store.data.room.exodus.TrackerDao
import com.aurora.store.data.room.exodus.TrackerEntity
import com.aurora.store.data.room.favourite.Favourite
import com.aurora.store.data.room.favourite.FavouriteDao
import com.aurora.store.data.room.review.LocalReview
@@ -28,9 +30,10 @@ import com.aurora.store.data.room.update.UpdateDao
IgnoredUpdate::class,
LocalReview::class,
Account::class,
AppAccountBinding::class
AppAccountBinding::class,
TrackerEntity::class
],
version = 10,
version = 12,
exportSchema = true
)
@TypeConverters(DownloadConverter::class, AccountConverter::class)
@@ -42,4 +45,5 @@ abstract class AuroraDatabase : RoomDatabase() {
abstract fun reviewDao(): ReviewDao
abstract fun accountDao(): AccountDao
abstract fun appAccountBindingDao(): AppAccountBindingDao
abstract fun trackerDao(): TrackerDao
}

View File

@@ -46,6 +46,14 @@ object MigrationHelper {
override fun migrate(db: SupportSQLiteDatabase) = migrateFrom9To10(db)
}
val MIGRATION_10_11 = object : Migration(10, 11) {
override fun migrate(db: SupportSQLiteDatabase) = migrateFrom10To11(db)
}
val MIGRATION_11_12 = object : Migration(11, 12) {
override fun migrate(db: SupportSQLiteDatabase) = migrateFrom11To12(db)
}
private const val TAG = "MigrationHelper"
private fun migrateFrom1To2(database: SupportSQLiteDatabase) {
@@ -241,4 +249,52 @@ object MigrationHelper {
database.endTransaction()
}
}
/**
* Add the exodus_tracker table for the API-synced tracker dictionary.
*/
private fun migrateFrom10To11(database: SupportSQLiteDatabase) {
database.beginTransaction()
try {
database.execSQL(
"CREATE TABLE IF NOT EXISTS `exodus_tracker` (" +
"`id` INTEGER NOT NULL, " +
"`name` TEXT NOT NULL, " +
"`url` TEXT NOT NULL, " +
"`signature` TEXT NOT NULL, " +
"`date` TEXT NOT NULL, " +
"PRIMARY KEY(`id`))"
)
database.setTransactionSuccessful()
} catch (exception: Exception) {
Log.e(TAG, "Failed while migrating from database version 10 to 11", exception)
} finally {
database.endTransaction()
}
}
/**
* Add a categories column to the exodus_tracker table, recreating it empty for the next sync.
*/
private fun migrateFrom11To12(database: SupportSQLiteDatabase) {
database.beginTransaction()
try {
database.execSQL("DROP TABLE IF EXISTS `exodus_tracker`")
database.execSQL(
"CREATE TABLE IF NOT EXISTS `exodus_tracker` (" +
"`id` INTEGER NOT NULL, " +
"`name` TEXT NOT NULL, " +
"`url` TEXT NOT NULL, " +
"`signature` TEXT NOT NULL, " +
"`date` TEXT NOT NULL, " +
"`categories` TEXT NOT NULL, " +
"PRIMARY KEY(`id`))"
)
database.setTransactionSuccessful()
} catch (exception: Exception) {
Log.e(TAG, "Failed while migrating from database version 11 to 12", exception)
} finally {
database.endTransaction()
}
}
}

View File

@@ -2,6 +2,8 @@ package com.aurora.store.data.room
import android.content.Context
import androidx.room.Room
import com.aurora.store.data.room.MigrationHelper.MIGRATION_10_11
import com.aurora.store.data.room.MigrationHelper.MIGRATION_11_12
import com.aurora.store.data.room.MigrationHelper.MIGRATION_1_2
import com.aurora.store.data.room.MigrationHelper.MIGRATION_2_3
import com.aurora.store.data.room.MigrationHelper.MIGRATION_3_4
@@ -16,6 +18,7 @@ import com.aurora.store.data.room.account.AccountDao
import com.aurora.store.data.room.account.AppAccountBindingDao
import com.aurora.store.data.room.download.DownloadConverter
import com.aurora.store.data.room.download.DownloadDao
import com.aurora.store.data.room.exodus.TrackerDao
import com.aurora.store.data.room.favourite.FavouriteDao
import com.aurora.store.data.room.review.ReviewDao
import com.aurora.store.data.room.update.IgnoredUpdateDao
@@ -49,7 +52,9 @@ object RoomModule {
MIGRATION_6_7,
MIGRATION_7_8,
MIGRATION_8_9,
MIGRATION_9_10
MIGRATION_9_10,
MIGRATION_10_11,
MIGRATION_11_12
)
.addTypeConverter(downloadConverter)
.addTypeConverter(accountConverter)
@@ -79,4 +84,7 @@ object RoomModule {
@Provides
fun providesAppAccountBindingDao(auroraDatabase: AuroraDatabase): AppAccountBindingDao =
auroraDatabase.appAccountBindingDao()
@Provides
fun providesTrackerDao(auroraDatabase: AuroraDatabase): TrackerDao = auroraDatabase.trackerDao()
}

View File

@@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.data.room.exodus
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
@Dao
interface TrackerDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(trackers: List<TrackerEntity>)
@Query("DELETE FROM exodus_tracker")
suspend fun deleteAll()
@Query("SELECT * FROM exodus_tracker WHERE id IN (:ids)")
suspend fun getByIds(ids: List<Int>): List<TrackerEntity>
@Query("SELECT COUNT(*) FROM exodus_tracker")
suspend fun count(): Int
@Transaction
suspend fun purgeAndInsert(trackers: List<TrackerEntity>) {
deleteAll()
insertAll(trackers)
}
}

View File

@@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.data.room.exodus
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "exodus_tracker")
data class TrackerEntity(
@PrimaryKey val id: Int,
val name: String,
val url: String,
val signature: String,
val date: String,
val categories: String
)

View File

@@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2026 Aurora OSS
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.aurora.store.data.work
import android.content.Context
import android.util.Log
import androidx.hilt.work.HiltWorker
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.aurora.extensions.TAG
import com.aurora.store.data.ExodusRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@HiltWorker
class ExodusTrackerWorker @AssistedInject constructor(
private val exodusRepository: ExodusRepository,
@Assisted context: Context,
@Assisted workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result = try {
exodusRepository.syncTrackersIfStale()
Result.success()
} catch (exception: Exception) {
Log.e(TAG, "Failed to sync exodus trackers", exception)
Result.retry()
}
companion object {
private const val EXODUS_TRACKER_WORKER = "EXODUS_TRACKER_WORKER"
fun enqueue(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val work = OneTimeWorkRequestBuilder<ExodusTrackerWorker>()
.setConstraints(constraints)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork(EXODUS_TRACKER_WORKER, ExistingWorkPolicy.KEEP, work)
}
}
}

View File

@@ -1,49 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* Aurora Store 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 2 of the License, or
* (at your option) any later version.
*
* Aurora Store 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 Aurora Store. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.aurora.store.module
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.nio.charset.StandardCharsets
import javax.inject.Singleton
import org.json.JSONArray
import org.json.JSONObject
@Module
@InstallIn(SingletonComponent::class)
object ExodusModule {
@Provides
@Singleton
fun providesLocalTrackersInfo(@ApplicationContext context: Context): JSONObject {
val inputStream = context.assets.open("exodus_trackers.json")
val bytes = ByteArray(inputStream.available())
inputStream.read(bytes)
inputStream.close()
val json = String(bytes, StandardCharsets.UTF_8)
val jsonArray = JSONArray(json)
return jsonArray.getJSONObject(0)
}
}

View File

@@ -68,6 +68,7 @@ object Preferences {
const val PREFERENCE_UPDATES_EXTENDED = "PREFERENCE_UPDATES_EXTENDED"
const val PREFERENCE_UPDATES_AUTO = "PREFERENCE_UPDATES_AUTO"
const val PREFERENCE_UPDATES_CHECK_INTERVAL = "PREFERENCE_UPDATES_CHECK_INTERVAL"
const val PREFERENCE_UPDATES_WARN_TRACKERS = "PREFERENCE_UPDATES_WARN_TRACKERS"
const val PREFERENCES_UPDATES_RESTRICTIONS = "PREFERENCES_UPDATES_RESTRICTIONS"
const val PREFERENCES_UPDATES_RESTRICTIONS_METERED = "PREFERENCES_UPDATES_RESTRICTIONS_METERED"
const val PREFERENCES_UPDATES_RESTRICTIONS_IDLE = "PREFERENCES_UPDATES_RESTRICTIONS_IDLE"
@@ -92,6 +93,8 @@ object Preferences {
const val PREFERENCE_CONFIRM_EXTERNAL_DEEPLINK = "PREFERENCE_CONFIRM_EXTERNAL_DEEPLINK"
const val PREFERENCE_LAST_TRACKER_SYNC = "PREFERENCE_LAST_TRACKER_SYNC"
private var prefs: SharedPreferences? = null
fun getPrefs(context: Context): SharedPreferences = when (BuildConfig.FLAVOR) {

View File

@@ -6,44 +6,21 @@
package com.aurora.store.viewmodel.details
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aurora.store.data.ExodusRepository
import com.aurora.store.data.model.ExodusTracker
import com.aurora.store.data.model.Report
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.json.JSONObject
@HiltViewModel
class ExodusViewModel @Inject constructor(
private val exodusTrackers: JSONObject
private val exodusRepository: ExodusRepository
) : ViewModel() {
private val _trackers = MutableStateFlow<List<ExodusTracker>>(emptyList())
val trackers = _trackers.asStateFlow()
fun getExodusTrackersFromReport(report: Report) {
viewModelScope.launch(Dispatchers.IO) {
val trackerObjects = report.trackers.map {
exodusTrackers.getJSONObject(it.toString())
}.toList()
_trackers.value = trackerObjects.map {
ExodusTracker(
id = it.getInt("id"),
name = it.getString("name"),
url = it.getString("website"),
signature = it.getString("code_signature"),
date = it.getString("creation_date"),
description = it.getString("description"),
networkSignature = it.getString("network_signature"),
documentation = listOf(it.getString("documentation")),
categories = listOf(it.getString("categories"))
)
}.toList()
}
}
/**
* Resolves the tracker ids of [report] to their details via the local tracker table,
* best-effort (ids missing from the table resolve to a `Tracker #<id>` placeholder).
*/
suspend fun resolveTrackers(report: Report): List<ExodusTracker> =
exodusRepository.resolveTrackers(report.trackers)
}