diff --git a/libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/2.json b/libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/2.json new file mode 100644 index 000000000..ecb4afb99 --- /dev/null +++ b/libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/2.json @@ -0,0 +1,1094 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "6f1a1580d37e51593441ffa87a858914", + "entities": [ + { + "tableName": "CoreRepository", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT, `address` TEXT NOT NULL, `webBaseUrl` TEXT, `timestamp` INTEGER NOT NULL, `version` INTEGER, `formatVersion` TEXT, `maxAge` INTEGER, `description` TEXT NOT NULL, `certificate` TEXT)", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "webBaseUrl", + "columnName": "webBaseUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "formatVersion", + "columnName": "formatVersion", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maxAge", + "columnName": "maxAge", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificate", + "columnName": "certificate", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "repoId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Mirror", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `url` TEXT NOT NULL, `location` TEXT, PRIMARY KEY(`repoId`, `url`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "url" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CoreRepository", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId" + ], + "referencedColumns": [ + "repoId" + ] + } + ] + }, + { + "tableName": "AntiFeature", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `id` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `id`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CoreRepository", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId" + ], + "referencedColumns": [ + "repoId" + ] + } + ] + }, + { + "tableName": "Category", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `id` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `id`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CoreRepository", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId" + ], + "referencedColumns": [ + "repoId" + ] + } + ] + }, + { + "tableName": "ReleaseChannel", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `id` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `id`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CoreRepository", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId" + ], + "referencedColumns": [ + "repoId" + ] + } + ] + }, + { + "tableName": "RepositoryPreferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `lastUpdated` INTEGER, `lastETag` TEXT, `userMirrors` TEXT, `disabledMirrors` TEXT, `username` TEXT, `password` TEXT, PRIMARY KEY(`repoId`))", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastETag", + "columnName": "lastETag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "userMirrors", + "columnName": "userMirrors", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "disabledMirrors", + "columnName": "disabledMirrors", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AppMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `added` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, `name` TEXT, `summary` TEXT, `description` TEXT, `localizedName` TEXT, `localizedSummary` TEXT, `webSite` TEXT, `changelog` TEXT, `license` TEXT, `sourceCode` TEXT, `issueTracker` TEXT, `translation` TEXT, `preferredSigner` TEXT, `video` TEXT, `authorName` TEXT, `authorEmail` TEXT, `authorWebSite` TEXT, `authorPhone` TEXT, `donate` TEXT, `liberapayID` TEXT, `liberapay` TEXT, `openCollective` TEXT, `bitcoin` TEXT, `litecoin` TEXT, `flattrID` TEXT, `categories` TEXT, `isCompatible` INTEGER NOT NULL, PRIMARY KEY(`repoId`, `packageName`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "added", + "columnName": "added", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "summary", + "columnName": "summary", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localizedName", + "columnName": "localizedName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "localizedSummary", + "columnName": "localizedSummary", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "webSite", + "columnName": "webSite", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "changelog", + "columnName": "changelog", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "license", + "columnName": "license", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sourceCode", + "columnName": "sourceCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "issueTracker", + "columnName": "issueTracker", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "translation", + "columnName": "translation", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "preferredSigner", + "columnName": "preferredSigner", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "video", + "columnName": "video", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorEmail", + "columnName": "authorEmail", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorWebSite", + "columnName": "authorWebSite", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorPhone", + "columnName": "authorPhone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "donate", + "columnName": "donate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "liberapayID", + "columnName": "liberapayID", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "liberapay", + "columnName": "liberapay", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "openCollective", + "columnName": "openCollective", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bitcoin", + "columnName": "bitcoin", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "litecoin", + "columnName": "litecoin", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "flattrID", + "columnName": "flattrID", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "categories", + "columnName": "categories", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isCompatible", + "columnName": "isCompatible", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "packageName" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "CoreRepository", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId" + ], + "referencedColumns": [ + "repoId" + ] + } + ] + }, + { + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "simple", + "tokenizerArgs": [], + "contentTable": "AppMetadata", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_BEFORE_UPDATE BEFORE UPDATE ON `AppMetadata` BEGIN DELETE FROM `AppMetadataFts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_BEFORE_DELETE BEFORE DELETE ON `AppMetadata` BEGIN DELETE FROM `AppMetadataFts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_AFTER_UPDATE AFTER UPDATE ON `AppMetadata` BEGIN INSERT INTO `AppMetadataFts`(`docid`, `repoId`, `packageName`, `localizedName`, `localizedSummary`) VALUES (NEW.`rowid`, NEW.`repoId`, NEW.`packageName`, NEW.`localizedName`, NEW.`localizedSummary`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_AFTER_INSERT AFTER INSERT ON `AppMetadata` BEGIN INSERT INTO `AppMetadataFts`(`docid`, `repoId`, `packageName`, `localizedName`, `localizedSummary`) VALUES (NEW.`rowid`, NEW.`repoId`, NEW.`packageName`, NEW.`localizedName`, NEW.`localizedSummary`); END" + ], + "tableName": "AppMetadataFts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `localizedName` TEXT, `localizedSummary` TEXT, content=`AppMetadata`)", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "localizedName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "summary", + "columnName": "localizedSummary", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "LocalizedFile", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `type` TEXT NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, `sha256` TEXT, `size` INTEGER, `ipfsCidV1` TEXT, PRIMARY KEY(`repoId`, `packageName`, `type`, `locale`), FOREIGN KEY(`repoId`, `packageName`) REFERENCES `AppMetadata`(`repoId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sha256", + "columnName": "sha256", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ipfsCidV1", + "columnName": "ipfsCidV1", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "packageName", + "type", + "locale" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AppMetadata", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId", + "packageName" + ], + "referencedColumns": [ + "repoId", + "packageName" + ] + } + ] + }, + { + "tableName": "LocalizedFileList", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `type` TEXT NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, `sha256` TEXT, `size` INTEGER, `ipfsCidV1` TEXT, PRIMARY KEY(`repoId`, `packageName`, `type`, `locale`, `name`), FOREIGN KEY(`repoId`, `packageName`) REFERENCES `AppMetadata`(`repoId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sha256", + "columnName": "sha256", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ipfsCidV1", + "columnName": "ipfsCidV1", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "packageName", + "type", + "locale", + "name" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AppMetadata", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId", + "packageName" + ], + "referencedColumns": [ + "repoId", + "packageName" + ] + } + ] + }, + { + "tableName": "Version", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `versionId` TEXT NOT NULL, `added` INTEGER NOT NULL, `releaseChannels` TEXT, `antiFeatures` TEXT, `whatsNew` TEXT, `isCompatible` INTEGER NOT NULL, `file_name` TEXT NOT NULL, `file_sha256` TEXT NOT NULL, `file_size` INTEGER, `file_ipfsCidV1` TEXT, `src_name` TEXT, `src_sha256` TEXT, `src_size` INTEGER, `src_ipfsCidV1` TEXT, `manifest_versionName` TEXT NOT NULL, `manifest_versionCode` INTEGER NOT NULL, `manifest_maxSdkVersion` INTEGER, `manifest_nativecode` TEXT, `manifest_features` TEXT, `manifest_usesSdk_minSdkVersion` INTEGER, `manifest_usesSdk_targetSdkVersion` INTEGER, `manifest_signer_sha256` TEXT, `manifest_signer_hasMultipleSigners` INTEGER, PRIMARY KEY(`repoId`, `packageName`, `versionId`), FOREIGN KEY(`repoId`, `packageName`) REFERENCES `AppMetadata`(`repoId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionId", + "columnName": "versionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "added", + "columnName": "added", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseChannels", + "columnName": "releaseChannels", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "antiFeatures", + "columnName": "antiFeatures", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "whatsNew", + "columnName": "whatsNew", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isCompatible", + "columnName": "isCompatible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "file.name", + "columnName": "file_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "file.sha256", + "columnName": "file_sha256", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "file.size", + "columnName": "file_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "file.ipfsCidV1", + "columnName": "file_ipfsCidV1", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "src.name", + "columnName": "src_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "src.sha256", + "columnName": "src_sha256", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "src.size", + "columnName": "src_size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "src.ipfsCidV1", + "columnName": "src_ipfsCidV1", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "manifest.versionName", + "columnName": "manifest_versionName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "manifest.versionCode", + "columnName": "manifest_versionCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manifest.maxSdkVersion", + "columnName": "manifest_maxSdkVersion", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "manifest.nativecode", + "columnName": "manifest_nativecode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "manifest.features", + "columnName": "manifest_features", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "manifest.usesSdk.minSdkVersion", + "columnName": "manifest_usesSdk_minSdkVersion", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "manifest.usesSdk.targetSdkVersion", + "columnName": "manifest_usesSdk_targetSdkVersion", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "manifest.signer.sha256", + "columnName": "manifest_signer_sha256", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "manifest.signer.hasMultipleSigners", + "columnName": "manifest_signer_hasMultipleSigners", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "packageName", + "versionId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "AppMetadata", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId", + "packageName" + ], + "referencedColumns": [ + "repoId", + "packageName" + ] + } + ] + }, + { + "tableName": "VersionedString", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `versionId` TEXT NOT NULL, `type` TEXT NOT NULL, `name` TEXT NOT NULL, `version` INTEGER, PRIMARY KEY(`repoId`, `packageName`, `versionId`, `type`, `name`), FOREIGN KEY(`repoId`, `packageName`, `versionId`) REFERENCES `Version`(`repoId`, `packageName`, `versionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionId", + "columnName": "versionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "packageName", + "versionId", + "type", + "name" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Version", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId", + "packageName", + "versionId" + ], + "referencedColumns": [ + "repoId", + "packageName", + "versionId" + ] + } + ] + }, + { + "tableName": "AppPrefs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `ignoreVersionCodeUpdate` INTEGER NOT NULL, `preferredRepoId` INTEGER, `appPrefReleaseChannels` TEXT, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ignoreVersionCodeUpdate", + "columnName": "ignoreVersionCodeUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "preferredRepoId", + "columnName": "preferredRepoId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "appPrefReleaseChannels", + "columnName": "appPrefReleaseChannels", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "packageName" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [ + { + "viewName": "LocalizedIcon", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM LocalizedFile WHERE type='icon'" + }, + { + "viewName": "HighestVersion", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT repoId, packageName, antiFeatures FROM Version\n GROUP BY repoId, packageName HAVING MAX(manifest_versionCode)" + } + ], + "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, '6f1a1580d37e51593441ffa87a858914')" + ] + } +} \ No newline at end of file diff --git a/libs/database/src/dbTest/java/org/fdroid/database/AppDaoTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/AppDaoTest.kt index d825b6a83..5c3d0aa24 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/AppDaoTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/AppDaoTest.kt @@ -62,6 +62,28 @@ internal class AppDaoTest : AppTest() { assertEquals(0, appDao.countLocalizedFileLists()) } + @Test + fun testAppRepoPref() { + // insert same app into three repos (repoId1 has highest weight) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + val repoId3 = repoDao.insertOrReplace(getRandomRepo()) + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageName, app1, locales) + appDao.insert(repoId2, packageName, app2, locales) + appDao.insert(repoId3, packageName, app3, locales) + + // app from repo with highest weight is returned, if no prefs are set + assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + + // prefer repo3 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3)) + assertEquals(app3, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + + // prefer repo1 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) + assertEquals(app1, appDao.getApp(packageName).getOrFail()?.toMetadataV2()?.sort()) + } + @Test fun testGetSameAppFromTwoReposOneDisabled() { // insert same app into two repos (repoId2 has highest weight) @@ -149,6 +171,13 @@ internal class AppDaoTest : AppTest() { assertEquals(3, appDao.getNumberOfAppsInCategory("A")) assertEquals(2, appDao.getNumberOfAppsInCategory("B")) assertEquals(0, appDao.getNumberOfAppsInCategory("C")) + + // app1 as a variant of app2 in another repo will show one more app in B + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId2, packageName2, app1, locales) + assertEquals(3, appDao.getNumberOfAppsInCategory("A")) + assertEquals(3, appDao.getNumberOfAppsInCategory("B")) + assertEquals(0, appDao.getNumberOfAppsInCategory("C")) } @Test diff --git a/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt index a989cc401..b7dce5948 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/AppListItemsTest.kt @@ -451,6 +451,48 @@ internal class AppListItemsTest : AppTest() { } } + @Test + fun testFromRepoFromAppPrefs() { + // insert same app into three repos (repoId1 has highest weight) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + val repoId3 = repoDao.insertOrReplace(getRandomRepo()) + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId2, packageName, app2, locales) + appDao.insert(repoId1, packageName, app1, locales) + appDao.insert(repoId3, packageName, app3, locales) + + // app from repo1 with highest weight gets returned + getItems { apps -> + assertEquals(1, apps.size) + assertEquals(packageName, apps[0].packageName) + assertEquals(app1, apps[0]) + } + + // prefer repo3 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3)) + getItems { apps -> + assertEquals(1, apps.size) + assertEquals(packageName, apps[0].packageName) + assertEquals(app3, apps[0]) + } + + // prefer repo2 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2)) + getItems { apps -> + assertEquals(1, apps.size) + assertEquals(packageName, apps[0].packageName) + assertEquals(app2, apps[0]) + } + + // prefer repo1 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) + getItems { apps -> + assertEquals(1, apps.size) + assertEquals(packageName, apps[0].packageName) + assertEquals(app1, apps[0]) + } + } + @Test fun testOnlyFromGivenCategories() { // insert three apps @@ -476,6 +518,16 @@ internal class AppListItemsTest : AppTest() { ).forEach { apps -> assertEquals(0, apps.size) } + + // we'll add app1 as a variant of app2, so it should be in category B as well + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId2, packageName2, app1, locales) + listOf( + appDao.getAppListItemsByName("B").getOrFail(), + appDao.getAppListItemsByLastUpdated("B").getOrFail(), + ).forEach { apps -> + assertEquals(3, apps.size) // all apps are in B now + } } @Test diff --git a/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt index ad7aa0c72..25fc58f1a 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/AppOverviewItemsTest.kt @@ -159,6 +159,63 @@ internal class AppOverviewItemsTest : AppTest() { } } + @Test + fun testGetByRepoPref() { + // insert same app into three repos (repoId1 has highest weight) + val repoId2 = repoDao.insertOrReplace(getRandomRepo()) + val repoId3 = repoDao.insertOrReplace(getRandomRepo()) + val repoId1 = repoDao.insertOrReplace(getRandomRepo()) + appDao.insert(repoId1, packageName, app1, locales) + appDao.insert(repoId2, packageName, app2, locales) + appDao.insert(repoId3, packageName, app3, locales) + + // app is returned correctly from repo1 + appDao.getAppOverviewItems().getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) + } + appDao.getAppOverviewItems("A").getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) + } + + // prefer repo3 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId3)) + appDao.getAppOverviewItems().getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app3, apps[0]) + } + appDao.getAppOverviewItems("B").getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app3, apps[0]) + } + + // prefer repo2 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId2)) + appDao.getAppOverviewItems().getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app2, apps[0]) + } + appDao.getAppOverviewItems("A").getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app2, apps[0]) + } + appDao.getAppOverviewItems("B").getOrFail().let { apps -> + assertEquals(0, apps.size) // app2 is not in category B + } + + // prefer repo1 for this app + appPrefsDao.update(AppPrefs(packageName, preferredRepoId = repoId1)) + appDao.getAppOverviewItems().getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) + } + appDao.getAppOverviewItems("A").getOrFail().let { apps -> + assertEquals(1, apps.size) + assertEquals(app1, apps[0]) + } + } + @Test fun testSortOrder() { // insert two apps with one version each diff --git a/libs/database/src/dbTest/java/org/fdroid/database/DbTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/DbTest.kt index 63baa388c..ddcec7551 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/DbTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/DbTest.kt @@ -10,6 +10,7 @@ import io.mockk.every import io.mockk.mockkObject import kotlinx.coroutines.Dispatchers import org.fdroid.database.TestUtils.assertRepoEquals +import org.fdroid.database.TestUtils.getOrFail import org.fdroid.database.TestUtils.toMetadataV2 import org.fdroid.database.TestUtils.toPackageVersionV2 import org.fdroid.index.v1.IndexV1StreamProcessor @@ -111,7 +112,7 @@ internal abstract class DbTest { packageV2.metadata, appDao.getApp(repoId, packageName)?.toMetadataV2()?.sort() ) - val versions = versionDao.getAppVersions(repoId, packageName).map { + val versions = versionDao.getAppVersions(repoId, packageName).getOrFail().map { it.toPackageVersionV2() }.associateBy { it.file.sha256 } assertEquals(packageV2.versions.size, versions.size, "number of versions") diff --git a/libs/database/src/dbTest/java/org/fdroid/database/RepositoryDaoTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/RepositoryDaoTest.kt index f041fd2a7..2b284beb6 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/RepositoryDaoTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/RepositoryDaoTest.kt @@ -255,7 +255,7 @@ internal class RepositoryDaoTest : DbTest() { // data is there as expected assertEquals(1, repoDao.getRepositories().size) assertEquals(1, appDao.getAppMetadata().size) - assertEquals(1, versionDao.getAppVersions(repoId, packageName).size) + assertEquals(1, versionDao.getAppVersions(repoId, packageName).getOrFail().size) assertTrue(versionDao.getVersionedStrings(repoId, packageName).isNotEmpty()) // clearing the repo removes apps and versions @@ -264,7 +264,7 @@ internal class RepositoryDaoTest : DbTest() { assertEquals(0, appDao.countApps()) assertEquals(0, appDao.countLocalizedFiles()) assertEquals(0, appDao.countLocalizedFileLists()) - assertEquals(0, versionDao.getAppVersions(repoId, packageName).size) + assertEquals(0, versionDao.getAppVersions(repoId, packageName).getOrFail().size) assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size) // preferences are not touched by clearing assertEquals(repositoryPreferences, repoDao.getRepositoryPreferences(repoId)) diff --git a/libs/database/src/dbTest/java/org/fdroid/database/VersionTest.kt b/libs/database/src/dbTest/java/org/fdroid/database/VersionTest.kt index 8599948a1..244dcf79d 100644 --- a/libs/database/src/dbTest/java/org/fdroid/database/VersionTest.kt +++ b/libs/database/src/dbTest/java/org/fdroid/database/VersionTest.kt @@ -35,6 +35,22 @@ internal class VersionTest : DbTest() { versionId2 to packageVersion2, ) + private fun getAppVersion1(repoId: Long): AppVersion { + val version = getVersion1(repoId) + return AppVersion( + version = version, + versionedStrings = packageVersion1.manifest.getVersionedStrings(version), + ) + } + + private fun getAppVersion2(repoId: Long): AppVersion { + val version = getVersion2(repoId) + return AppVersion( + version = version, + versionedStrings = packageVersion2.manifest.getVersionedStrings(version), + ) + } + private fun getVersion1(repoId: Long) = packageVersion1.toVersion(repoId, packageName, versionId1, isCompatible1) @@ -55,25 +71,22 @@ internal class VersionTest : DbTest() { appDao.insert(repoId, packageName, getRandomMetadataV2()) versionDao.insert(repoId, packageName, versionId1, packageVersion1, isCompatible1) - val appVersions = versionDao.getAppVersions(repoId, packageName) + val appVersions = versionDao.getAppVersions(repoId, packageName).getOrFail() assertEquals(1, appVersions.size) - val appVersion = appVersions[0] - assertEquals(versionId1, appVersion.version.versionId) - assertEquals(getVersion1(repoId), appVersion.version) - val manifest = packageVersion1.manifest - assertEquals(manifest.usesPermission.toSet(), appVersion.usesPermission.toSet()) - assertEquals(manifest.usesPermissionSdk23.toSet(), appVersion.usesPermissionSdk23.toSet()) - assertEquals( - manifest.features.map { it.name }.toSet(), - appVersion.version.manifest.features?.toSet() - ) + assertEquals(getAppVersion1(repoId), appVersions[0]) + val manifest = packageVersion1.manifest val versionedStrings = versionDao.getVersionedStrings(repoId, packageName) val expectedSize = manifest.usesPermission.size + manifest.usesPermissionSdk23.size assertEquals(expectedSize, versionedStrings.size) + // getting version by repo produces same result + val versionsByRepo = versionDao.getAppVersions(repoId, packageName).getOrFail() + assertEquals(1, versionsByRepo.size) + assertEquals(getAppVersion1(repoId), versionsByRepo[0]) + versionDao.deleteAppVersion(repoId, packageName, versionId1) - assertEquals(0, versionDao.getAppVersions(repoId, packageName).size) + assertEquals(0, versionDao.getAppVersions(repoId, packageName).getOrFail().size) assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size) } @@ -86,39 +99,29 @@ internal class VersionTest : DbTest() { versionDao.insert(repoId, packageName, versionId2, packageVersion2, isCompatible2) // get app versions from DB and assign them correctly - val appVersions = versionDao.getAppVersions(packageName).getOrFail() - assertEquals(2, appVersions.size) - val appVersion = if (versionId1 == appVersions[0].version.versionId) { - appVersions[0] - } else appVersions[1] - val appVersion2 = if (versionId2 == appVersions[0].version.versionId) { - appVersions[0] - } else appVersions[1] + listOf( + versionDao.getAppVersions(packageName).getOrFail(), + versionDao.getAppVersions(repoId, packageName).getOrFail(), + ).forEach { appVersions -> + assertEquals(2, appVersions.size) + val appVersion = if (versionId1 == appVersions[0].version.versionId) { + appVersions[0] + } else appVersions[1] + val appVersion2 = if (versionId2 == appVersions[0].version.versionId) { + appVersions[0] + } else appVersions[1] - // check first version matches - assertEquals(getVersion1(repoId), appVersion.version) - val manifest = packageVersion1.manifest - assertEquals(manifest.usesPermission.toSet(), appVersion.usesPermission.toSet()) - assertEquals(manifest.usesPermissionSdk23.toSet(), appVersion.usesPermissionSdk23.toSet()) - assertEquals( - manifest.features.map { it.name }.toSet(), - appVersion.version.manifest.features?.toSet() - ) + // check first version matches + assertEquals(getAppVersion1(repoId), appVersion) - // check second version matches - assertEquals(getVersion2(repoId), appVersion2.version) - val manifest2 = packageVersion2.manifest - assertEquals(manifest2.usesPermission.toSet(), appVersion2.usesPermission.toSet()) - assertEquals(manifest2.usesPermissionSdk23.toSet(), - appVersion2.usesPermissionSdk23.toSet()) - assertEquals( - manifest.features.map { it.name }.toSet(), - appVersion.version.manifest.features?.toSet() - ) + // check second version matches + assertEquals(getAppVersion2(repoId), appVersion2) + } // delete app and check that all associated data also gets deleted appDao.deleteAppMetadata(repoId, packageName) - assertEquals(0, versionDao.getAppVersions(repoId, packageName).size) + assertEquals(0, versionDao.getAppVersions(packageName).getOrFail().size) + assertEquals(0, versionDao.getAppVersions(repoId, packageName).getOrFail().size) assertEquals(0, versionDao.getVersionedStrings(repoId, packageName).size) } @@ -138,6 +141,10 @@ internal class VersionTest : DbTest() { assertEquals(3, versionDao.getAppVersions(packageName).getOrFail().size) assertEquals(3, versionDao.getVersions(listOf(packageName)).size) + // query by repo only returns the versions from each repo + assertEquals(2, versionDao.getAppVersions(repoId, packageName).getOrFail().size) + assertEquals(1, versionDao.getAppVersions(repoId2, packageName).getOrFail().size) + // disable second repo repoDao.setRepositoryEnabled(repoId2, false) @@ -155,8 +162,10 @@ internal class VersionTest : DbTest() { versionDao.insert(repoId, packageName, versionId3, packageVersion3, true) val versions1 = versionDao.getAppVersions(packageName).getOrFail() val versions2 = versionDao.getVersions(listOf(packageName)) + val versions3 = versionDao.getAppVersions(repoId, packageName).getOrFail() assertEquals(3, versions1.size) assertEquals(3, versions2.size) + assertEquals(3, versions3.size) // check that they are sorted as expected listOf( @@ -166,6 +175,7 @@ internal class VersionTest : DbTest() { ).sortedDescending().forEachIndexed { i, versionCode -> assertEquals(versionCode, versions1[i].version.manifest.versionCode) assertEquals(versionCode, versions2[i].versionCode) + assertEquals(versionCode, versions3[i].version.manifest.versionCode) } } diff --git a/libs/database/src/main/java/org/fdroid/database/AppDao.kt b/libs/database/src/main/java/org/fdroid/database/AppDao.kt index 5f5e17c37..6a17deeb5 100644 --- a/libs/database/src/main/java/org/fdroid/database/AppDao.kt +++ b/libs/database/src/main/java/org/fdroid/database/AppDao.kt @@ -305,7 +305,9 @@ internal interface AppDaoInt : AppDao { @Transaction @Query("""SELECT ${AppMetadata.TABLE}.* FROM ${AppMetadata.TABLE} JOIN RepositoryPreferences AS pref USING (repoId) - WHERE packageName = :packageName AND pref.enabled = 1 + LEFT JOIN AppPrefs USING (packageName) + WHERE packageName = :packageName AND pref.enabled = 1 AND + COALESCE(preferredRepoId, repoId) = repoId ORDER BY pref.weight DESC LIMIT 1""") override fun getApp(packageName: String): LiveData @@ -341,7 +343,8 @@ internal interface AppDaoInt : AppDao { JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId) LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName) LEFT JOIN ${LocalizedIcon.TABLE} AS icon USING (repoId, packageName) - WHERE pref.enabled = 1 + LEFT JOIN AppPrefs USING (packageName) + WHERE pref.enabled = 1 AND COALESCE(preferredRepoId, repoId) = repoId GROUP BY packageName HAVING MAX(pref.weight) ORDER BY localizedName IS NULL ASC, icon.packageName IS NULL ASC, localizedSummary IS NULL ASC, app.lastUpdated DESC @@ -355,7 +358,9 @@ internal interface AppDaoInt : AppDao { JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId) LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName) LEFT JOIN ${LocalizedIcon.TABLE} AS icon USING (repoId, packageName) - WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%' + LEFT JOIN AppPrefs USING (packageName) + WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%' AND + COALESCE(preferredRepoId, repoId) = repoId GROUP BY packageName HAVING MAX(pref.weight) ORDER BY localizedName IS NULL ASC, icon.packageName IS NULL ASC, localizedSummary IS NULL ASC, app.lastUpdated DESC @@ -440,8 +445,10 @@ internal interface AppDaoInt : AppDao { FROM ${AppMetadata.TABLE} AS app JOIN ${AppMetadataFts.TABLE} USING (repoId, packageName) LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName) + LEFT JOIN AppPrefs USING (packageName) JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId) - WHERE pref.enabled = 1 AND ${AppMetadataFts.TABLE} MATCH :searchQuery + WHERE pref.enabled = 1 AND ${AppMetadataFts.TABLE} MATCH :searchQuery AND + COALESCE(preferredRepoId, repoId) = repoId GROUP BY packageName HAVING MAX(pref.weight)""") fun getAppListItems(searchQuery: String): LiveData> @@ -455,9 +462,11 @@ internal interface AppDaoInt : AppDao { FROM ${AppMetadata.TABLE} AS app JOIN ${AppMetadataFts.TABLE} USING (repoId, packageName) LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName) + LEFT JOIN AppPrefs USING (packageName) JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId) WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%' AND - ${AppMetadataFts.TABLE} MATCH :searchQuery + ${AppMetadataFts.TABLE} MATCH :searchQuery AND + COALESCE(preferredRepoId, repoId) = repoId GROUP BY packageName HAVING MAX(pref.weight)""") fun getAppListItems(category: String, searchQuery: String): LiveData> @@ -485,8 +494,9 @@ internal interface AppDaoInt : AppDao { version.antiFeatures, app.isCompatible, app.preferredSigner FROM ${AppMetadata.TABLE} AS app LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName) + LEFT JOIN AppPrefs USING (packageName) JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId) - WHERE pref.enabled = 1 + WHERE pref.enabled = 1 AND COALESCE(preferredRepoId, repoId) = repoId GROUP BY packageName HAVING MAX(pref.weight) ORDER BY localizedName COLLATE NOCASE ASC""") fun getAppListItemsByName(): LiveData> @@ -498,7 +508,8 @@ internal interface AppDaoInt : AppDao { FROM ${AppMetadata.TABLE} AS app JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId) LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName) - WHERE pref.enabled = 1 + LEFT JOIN AppPrefs USING (packageName) + WHERE pref.enabled = 1 AND COALESCE(preferredRepoId, repoId) = repoId GROUP BY packageName HAVING MAX(pref.weight) ORDER BY app.lastUpdated DESC""") fun getAppListItemsByLastUpdated(): LiveData> @@ -510,7 +521,9 @@ internal interface AppDaoInt : AppDao { FROM ${AppMetadata.TABLE} AS app JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId) LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName) - WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%' + LEFT JOIN AppPrefs USING (packageName) + WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%' AND + COALESCE(preferredRepoId, repoId) = repoId GROUP BY packageName HAVING MAX(pref.weight) ORDER BY app.lastUpdated DESC""") fun getAppListItemsByLastUpdated(category: String): LiveData> @@ -522,7 +535,9 @@ internal interface AppDaoInt : AppDao { FROM ${AppMetadata.TABLE} AS app JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId) LEFT JOIN ${HighestVersion.TABLE} AS version USING (repoId, packageName) - WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%' + LEFT JOIN AppPrefs USING (packageName) + WHERE pref.enabled = 1 AND categories LIKE '%,' || :category || ',%' AND + COALESCE(preferredRepoId, repoId) = repoId GROUP BY packageName HAVING MAX(pref.weight) ORDER BY localizedName COLLATE NOCASE ASC""") fun getAppListItemsByName(category: String): LiveData> @@ -556,7 +571,9 @@ internal interface AppDaoInt : AppDao { app.isCompatible, app.preferredSigner FROM ${AppMetadata.TABLE} AS app JOIN ${RepositoryPreferences.TABLE} AS pref USING (repoId) - WHERE pref.enabled = 1 AND packageName IN (:packageNames) + LEFT JOIN AppPrefs USING (packageName) + WHERE pref.enabled = 1 AND packageName IN (:packageNames) AND + COALESCE(preferredRepoId, repoId) = repoId GROUP BY packageName HAVING MAX(pref.weight) ORDER BY localizedName COLLATE NOCASE ASC""") fun getAppListItems(packageNames: List): LiveData> diff --git a/libs/database/src/main/java/org/fdroid/database/AppPrefs.kt b/libs/database/src/main/java/org/fdroid/database/AppPrefs.kt index 3b08bdc09..8b500b6e1 100644 --- a/libs/database/src/main/java/org/fdroid/database/AppPrefs.kt +++ b/libs/database/src/main/java/org/fdroid/database/AppPrefs.kt @@ -13,6 +13,7 @@ public data class AppPrefs( @PrimaryKey val packageName: String, override val ignoreVersionCodeUpdate: Long = 0, + val preferredRepoId: Long? = null, // This is named like this, because it hit a Room bug when joining with Version table // which had exactly the same field. internal val appPrefReleaseChannels: List? = null, diff --git a/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt b/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt index be228fb03..876fc43d6 100644 --- a/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt +++ b/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt @@ -3,6 +3,7 @@ package org.fdroid.database import android.content.res.Resources import androidx.core.os.ConfigurationCompat.getLocales import androidx.core.os.LocaleListCompat +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters @@ -14,7 +15,7 @@ import java.util.concurrent.Callable // When bumping this version, please make sure to add one (or more) migration(s) below! // Consider also providing tests for that migration. // Don't forget to commit the new schema to the git repo as well. - version = 1, + version = 2, entities = [ // repo CoreRepository::class, @@ -41,6 +42,7 @@ import java.util.concurrent.Callable exportSchema = true, autoMigrations = [ // add future migrations here (if they are easy enough to be done automatically) + AutoMigration(1, 2), ], ) @TypeConverters(Converters::class) diff --git a/libs/database/src/main/java/org/fdroid/database/VersionDao.kt b/libs/database/src/main/java/org/fdroid/database/VersionDao.kt index 1bf357e96..f5309a3e3 100644 --- a/libs/database/src/main/java/org/fdroid/database/VersionDao.kt +++ b/libs/database/src/main/java/org/fdroid/database/VersionDao.kt @@ -35,6 +35,12 @@ public interface VersionDao { * Returns a list of versions for the given [packageName] sorting by highest version code first. */ public fun getAppVersions(packageName: String): LiveData> + + /** + * Returns a list of versions from the repo identified by the given [repoId] + * for the given [packageName] sorting by highest version code first. + */ + public fun getAppVersions(repoId: Long, packageName: String): LiveData> } /** @@ -161,13 +167,12 @@ internal interface VersionDaoInt : VersionDao { ORDER BY manifest_versionCode DESC, pref.weight DESC""") override fun getAppVersions(packageName: String): LiveData> - /** - * Only use for testing, not sorted, does take disabled repos into account. - */ @Transaction + @RewriteQueriesToDropUnusedColumns @Query("""SELECT * FROM ${Version.TABLE} - WHERE repoId = :repoId AND packageName = :packageName""") - fun getAppVersions(repoId: Long, packageName: String): List + WHERE repoId = :repoId AND packageName = :packageName + ORDER BY manifest_versionCode DESC""") + override fun getAppVersions(repoId: Long, packageName: String): LiveData> @Query("""SELECT * FROM ${Version.TABLE} WHERE repoId = :repoId AND packageName = :packageName AND versionId = :versionId""")