updates: revamp updates UX
- Group updates into main / approval-required / incompatible / ignored - New ignored_update table (v8) with per-version skip support - isIncompatible flag on pristine system apps for HyperOS / GrapheneOS (v7) - requiresOwnershipTransfer() runtime check on Android 14+ - Rename AppMenuSheet -> AppUpdateSheet; drop MinimalApp, nav carries Update - Trim AppUpdateItem (no changelog dropdown / long-click) - Fix DownloadHelper.downloadsList recomposition flicker (stable StateFlow) - ExportWorker is now download-only
This commit is contained in:
289
app/schemas/com.aurora.store.data.room.AuroraDatabase/7.json
Normal file
289
app/schemas/com.aurora.store.data.room.AuroraDatabase/7.json
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 7,
|
||||||
|
"identityHash": "2fad890b7c37341bc2b26ef023b76124",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, '2fad890b7c37341bc2b26ef023b76124')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
312
app/schemas/com.aurora.store.data.room.AuroraDatabase/8.json
Normal file
312
app/schemas/com.aurora.store.data.room.AuroraDatabase/8.json
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 8,
|
||||||
|
"identityHash": "e21f5fa9464526f3ddfe0a7f58119d1a",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, 'e21f5fa9464526f3ddfe0a7f58119d1a')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,15 @@ val isVAndAbove: Boolean
|
|||||||
val isMIUI: Boolean
|
val isMIUI: Boolean
|
||||||
get() = !getSystemProperty("ro.miui.ui.version.name").isNullOrBlank()
|
get() = !getSystemProperty("ro.miui.ui.version.name").isNullOrBlank()
|
||||||
|
|
||||||
|
val isHyperOS: Boolean
|
||||||
|
get() = !getSystemProperty("ro.mi.os.version.name").isNullOrBlank() ||
|
||||||
|
!getSystemProperty("ro.mi.os.version.code").isNullOrBlank()
|
||||||
|
|
||||||
|
val isGrapheneOS: Boolean
|
||||||
|
get() = Build.FINGERPRINT.contains("GrapheneOS", ignoreCase = true) ||
|
||||||
|
Build.HOST.contains("grapheneos", ignoreCase = true) ||
|
||||||
|
getSystemProperty("ro.build.flavor")?.contains("grapheneos", ignoreCase = true) == true
|
||||||
|
|
||||||
val isHuawei: Boolean
|
val isHuawei: Boolean
|
||||||
get() = Build.MANUFACTURER.lowercase(Locale.getDefault()).contains("huawei") ||
|
get() = Build.MANUFACTURER.lowercase(Locale.getDefault()).contains("huawei") ||
|
||||||
Build.HARDWARE.lowercase(Locale.getDefault()).contains("kirin") ||
|
Build.HARDWARE.lowercase(Locale.getDefault()).contains("kirin") ||
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
package com.aurora.store.compose.composable
|
package com.aurora.store.compose.composable
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -68,13 +69,15 @@ fun SectionHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Section header row with a right-aligned action button (replaces UpdateHeaderView).
|
* Section header row with optional subtitle and right-aligned action button
|
||||||
|
* (replaces UpdateHeaderView). Pass `action = null` to omit the button entirely.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun SectionHeaderWithAction(
|
fun SectionHeaderWithAction(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
title: String,
|
title: String,
|
||||||
action: String,
|
action: String? = null,
|
||||||
|
subtitle: String? = null,
|
||||||
onAction: () -> Unit = {}
|
onAction: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@@ -87,18 +90,30 @@ fun SectionHeaderWithAction(
|
|||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
)
|
||||||
|
if (!subtitle.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!action.isNullOrBlank()) {
|
||||||
TextButton(onClick = onAction) {
|
TextButton(onClick = onAction) {
|
||||||
Text(action)
|
Text(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewWrapper(ThemePreviewProvider::class)
|
@PreviewWrapper(ThemePreviewProvider::class)
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ fun StreamCarousel(
|
|||||||
lazyListState: LazyListState = rememberLazyListState(),
|
lazyListState: LazyListState = rememberLazyListState(),
|
||||||
onHeaderClick: (StreamCluster) -> Unit = {},
|
onHeaderClick: (StreamCluster) -> Unit = {},
|
||||||
onAppClick: (App) -> Unit = {},
|
onAppClick: (App) -> Unit = {},
|
||||||
onAppLongClick: (App) -> Unit = {},
|
|
||||||
onClusterScrolled: (StreamCluster) -> Unit = {},
|
onClusterScrolled: (StreamCluster) -> Unit = {},
|
||||||
onScrolledToEnd: () -> Unit = {}
|
onScrolledToEnd: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
@@ -118,7 +117,6 @@ fun StreamCarousel(
|
|||||||
ClusterRow(
|
ClusterRow(
|
||||||
cluster = cluster,
|
cluster = cluster,
|
||||||
onAppClick = onAppClick,
|
onAppClick = onAppClick,
|
||||||
onAppLongClick = onAppLongClick,
|
|
||||||
onClusterScrolled = onClusterScrolled
|
onClusterScrolled = onClusterScrolled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -135,7 +133,6 @@ fun StreamCarousel(
|
|||||||
internal fun ClusterRow(
|
internal fun ClusterRow(
|
||||||
cluster: StreamCluster,
|
cluster: StreamCluster,
|
||||||
onAppClick: (App) -> Unit = {},
|
onAppClick: (App) -> Unit = {},
|
||||||
onAppLongClick: (App) -> Unit = {},
|
|
||||||
onClusterScrolled: (StreamCluster) -> Unit = {}
|
onClusterScrolled: (StreamCluster) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val rowState = rememberLazyListState()
|
val rowState = rememberLazyListState()
|
||||||
@@ -162,8 +159,7 @@ internal fun ClusterRow(
|
|||||||
) { _, app ->
|
) { _, app ->
|
||||||
AppListItem(
|
AppListItem(
|
||||||
app = app,
|
app = app,
|
||||||
onClick = { onAppClick(app) },
|
onClick = { onAppClick(app) }
|
||||||
onLongClick = { onAppLongClick(app) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (cluster.hasNext()) {
|
if (cluster.hasNext()) {
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.composable.app
|
package com.aurora.store.compose.composable.app
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
@@ -41,21 +40,14 @@ import com.aurora.store.compose.preview.ThemePreviewProvider
|
|||||||
* @param modifier The modifier to be applied to the composable
|
* @param modifier The modifier to be applied to the composable
|
||||||
* @param app [App] to display
|
* @param app [App] to display
|
||||||
* @param onClick Callback when the composable is clicked
|
* @param onClick Callback when the composable is clicked
|
||||||
* @param onLongClick Callback when the composable is long-clicked
|
|
||||||
* @see LargeAppListItem
|
* @see LargeAppListItem
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppListItem(
|
fun AppListItem(modifier: Modifier = Modifier, app: App, onClick: () -> Unit = {}) {
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
app: App,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
onLongClick: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(dimensionResource(R.dimen.icon_size_cluster))
|
.width(dimensionResource(R.dimen.icon_size_cluster))
|
||||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(all = dimensionResource(R.dimen.padding_xxsmall)),
|
.padding(all = dimensionResource(R.dimen.padding_xxsmall)),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
|
|
||||||
package com.aurora.store.compose.composable.app
|
package com.aurora.store.compose.composable.app
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -16,26 +14,15 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.requiredSize
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.fromHtml
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||||
@@ -52,9 +39,9 @@ fun AppUpdateItem(
|
|||||||
update: Update,
|
update: Update,
|
||||||
download: Download? = null,
|
download: Download? = null,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
onLongClick: () -> Unit = {},
|
|
||||||
onUpdate: () -> Unit = {},
|
onUpdate: () -> Unit = {},
|
||||||
onCancel: () -> Unit = {}
|
onCancel: () -> Unit = {},
|
||||||
|
onUnignore: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
val inProgress = download != null && download.status !in DownloadStatus.finished
|
val inProgress = download != null && download.status !in DownloadStatus.finished
|
||||||
val progress = if (download?.status == DownloadStatus.DOWNLOADING) {
|
val progress = if (download?.status == DownloadStatus.DOWNLOADING) {
|
||||||
@@ -62,13 +49,11 @@ fun AppUpdateItem(
|
|||||||
} else {
|
} else {
|
||||||
0f
|
0f
|
||||||
}
|
}
|
||||||
var changelogExpanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxWidth()) {
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||||
vertical = dimensionResource(R.dimen.padding_small)
|
vertical = dimensionResource(R.dimen.padding_small)
|
||||||
@@ -83,7 +68,7 @@ fun AppUpdateItem(
|
|||||||
progress = progress
|
progress = progress
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(dimensionResource(R.dimen.margin_small)))
|
Spacer(Modifier.width(dimensionResource(R.dimen.margin_medium)))
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = update.displayName,
|
text = update.displayName,
|
||||||
@@ -93,68 +78,39 @@ fun AppUpdateItem(
|
|||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = update.developerName,
|
text = update.developerName,
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "${CommonUtil.addSiPrefix(update.size)} • ${update.updatedOn}",
|
text = "${update.versionName} • ${CommonUtil.addSiPrefix(update.size)}",
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
Text(
|
maxLines = 1,
|
||||||
text = "${update.versionName} (${update.versionCode})",
|
overflow = TextOverflow.Ellipsis
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(dimensionResource(R.dimen.margin_small)))
|
Spacer(Modifier.width(dimensionResource(R.dimen.margin_small)))
|
||||||
if (inProgress) {
|
when {
|
||||||
|
onUnignore != null -> {
|
||||||
|
OutlinedButton(onClick = onUnignore) {
|
||||||
|
Text(stringResource(R.string.action_unignore))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inProgress -> {
|
||||||
OutlinedButton(onClick = onCancel) {
|
OutlinedButton(onClick = onCancel) {
|
||||||
Text(stringResource(R.string.action_cancel))
|
Text(stringResource(R.string.action_cancel))
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
Button(onClick = onUpdate) {
|
Button(onClick = onUpdate) {
|
||||||
Text(stringResource(R.string.action_update))
|
Text(stringResource(R.string.action_update))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { changelogExpanded = !changelogExpanded }
|
|
||||||
.padding(horizontal = dimensionResource(R.dimen.padding_medium)),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Spacer(Modifier.weight(1f))
|
|
||||||
IconButton(onClick = { changelogExpanded = !changelogExpanded }) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(
|
|
||||||
if (changelogExpanded) R.drawable.ic_arrow_up else R.drawable.ic_arrow_down
|
|
||||||
),
|
|
||||||
contentDescription = stringResource(R.string.expand)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(visible = changelogExpanded) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(
|
|
||||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
|
||||||
vertical = dimensionResource(R.dimen.padding_small)
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(dimensionResource(R.dimen.radius_small))
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = if (update.changelog.isNotEmpty()) {
|
|
||||||
AnnotatedString.fromHtml(update.changelog)
|
|
||||||
} else {
|
|
||||||
AnnotatedString(stringResource(R.string.details_changelog_unavailable))
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
modifier = Modifier.padding(dimensionResource(R.dimen.padding_medium))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ package com.aurora.store.compose.navigation
|
|||||||
|
|
||||||
import com.aurora.gplayapi.data.models.Category
|
import com.aurora.gplayapi.data.models.Category
|
||||||
import com.aurora.gplayapi.data.models.StreamCluster
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
import com.aurora.store.data.model.MinimalApp
|
|
||||||
import com.aurora.store.data.model.PermissionType
|
import com.aurora.store.data.model.PermissionType
|
||||||
|
import com.aurora.store.data.room.update.Update
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All navigation actions available to composable screens.
|
* All navigation actions available to composable screens.
|
||||||
@@ -20,7 +20,7 @@ sealed class Destination {
|
|||||||
|
|
||||||
data class AppDetails(val packageName: String) : Destination()
|
data class AppDetails(val packageName: String) : Destination()
|
||||||
data class DevProfile(val devId: String) : Destination()
|
data class DevProfile(val devId: String) : Destination()
|
||||||
data class AppMenu(val app: MinimalApp) : Destination()
|
data class AppUpdate(val update: Update) : Destination()
|
||||||
|
|
||||||
data object Search : Destination()
|
data object Search : Destination()
|
||||||
data object Downloads : Destination()
|
data object Downloads : Destination()
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ fun NavDisplay(startDestination: NavKey) {
|
|||||||
|
|
||||||
is Destination.AppDetails -> backstack.add(Screen.AppDetails(destination.packageName))
|
is Destination.AppDetails -> backstack.add(Screen.AppDetails(destination.packageName))
|
||||||
is Destination.DevProfile -> backstack.add(Screen.DevProfile(destination.devId))
|
is Destination.DevProfile -> backstack.add(Screen.DevProfile(destination.devId))
|
||||||
is Destination.AppMenu -> Unit
|
is Destination.AppUpdate -> Unit
|
||||||
is Destination.StreamBrowse -> backstack.add(Screen.StreamBrowse(destination.cluster))
|
is Destination.StreamBrowse -> backstack.add(Screen.StreamBrowse(destination.cluster))
|
||||||
|
|
||||||
Destination.Search -> backstack.add(Screen.Search)
|
Destination.Search -> backstack.add(Screen.Search)
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import com.aurora.gplayapi.helpers.contracts.StreamContract
|
|||||||
import com.aurora.gplayapi.helpers.contracts.TopChartsContract
|
import com.aurora.gplayapi.helpers.contracts.TopChartsContract
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.compose.navigation.Destination
|
import com.aurora.store.compose.navigation.Destination
|
||||||
import com.aurora.store.data.model.MinimalApp
|
|
||||||
import com.aurora.store.util.Preferences
|
import com.aurora.store.util.Preferences
|
||||||
import com.aurora.store.viewmodel.category.CategoryViewModel
|
import com.aurora.store.viewmodel.category.CategoryViewModel
|
||||||
import com.aurora.store.viewmodel.homestream.StreamViewModel
|
import com.aurora.store.viewmodel.homestream.StreamViewModel
|
||||||
@@ -100,7 +99,6 @@ fun AppsGamesScreen(
|
|||||||
pageType = pageType,
|
pageType = pageType,
|
||||||
viewModel = streamViewModel,
|
viewModel = streamViewModel,
|
||||||
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
|
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
|
||||||
onAppLongClick = { onNavigateTo(Destination.AppMenu(MinimalApp.fromApp(it))) },
|
|
||||||
onHeaderClick = { onNavigateTo(Destination.StreamBrowse(it)) },
|
onHeaderClick = { onNavigateTo(Destination.StreamBrowse(it)) },
|
||||||
onClusterScrolled = { cluster ->
|
onClusterScrolled = { cluster ->
|
||||||
streamViewModel.observeCluster(category(pageType), cluster)
|
streamViewModel.observeCluster(category(pageType), cluster)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ internal fun ForYouContent(
|
|||||||
pageType: Int,
|
pageType: Int,
|
||||||
viewModel: StreamViewModel,
|
viewModel: StreamViewModel,
|
||||||
onAppClick: (App) -> Unit,
|
onAppClick: (App) -> Unit,
|
||||||
onAppLongClick: (App) -> Unit,
|
|
||||||
onHeaderClick: (StreamCluster) -> Unit,
|
onHeaderClick: (StreamCluster) -> Unit,
|
||||||
onClusterScrolled: (StreamCluster) -> Unit,
|
onClusterScrolled: (StreamCluster) -> Unit,
|
||||||
onScrolledToEnd: () -> Unit
|
onScrolledToEnd: () -> Unit
|
||||||
@@ -43,7 +42,6 @@ internal fun ForYouContent(
|
|||||||
streamBundle = streamBundle?.get(category),
|
streamBundle = streamBundle?.get(category),
|
||||||
onHeaderClick = onHeaderClick,
|
onHeaderClick = onHeaderClick,
|
||||||
onAppClick = onAppClick,
|
onAppClick = onAppClick,
|
||||||
onAppLongClick = onAppLongClick,
|
|
||||||
onClusterScrolled = onClusterScrolled,
|
onClusterScrolled = onClusterScrolled,
|
||||||
onScrolledToEnd = onScrolledToEnd
|
onScrolledToEnd = onScrolledToEnd
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -43,12 +43,12 @@ import com.aurora.store.compose.navigation.Destination
|
|||||||
import com.aurora.store.compose.ui.apps.AppsGamesScreen
|
import com.aurora.store.compose.ui.apps.AppsGamesScreen
|
||||||
import com.aurora.store.compose.ui.commons.MoreSheet
|
import com.aurora.store.compose.ui.commons.MoreSheet
|
||||||
import com.aurora.store.compose.ui.commons.NetworkSheet
|
import com.aurora.store.compose.ui.commons.NetworkSheet
|
||||||
import com.aurora.store.compose.ui.sheets.AppMenuSheet
|
import com.aurora.store.compose.ui.sheets.AppUpdateSheet
|
||||||
import com.aurora.store.compose.ui.updates.UpdatesScreen
|
import com.aurora.store.compose.ui.updates.UpdatesScreen
|
||||||
import com.aurora.store.data.model.MinimalApp
|
|
||||||
import com.aurora.store.data.model.NetworkStatus
|
import com.aurora.store.data.model.NetworkStatus
|
||||||
import com.aurora.store.data.model.PermissionType
|
import com.aurora.store.data.model.PermissionType
|
||||||
import com.aurora.store.data.providers.PermissionProvider.Companion.isGranted
|
import com.aurora.store.data.providers.PermissionProvider.Companion.isGranted
|
||||||
|
import com.aurora.store.data.room.update.Update
|
||||||
import com.aurora.store.viewmodel.all.UpdatesViewModel
|
import com.aurora.store.viewmodel.all.UpdatesViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -88,12 +88,12 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var showMoreSheet by remember { mutableStateOf(false) }
|
var showMoreSheet by remember { mutableStateOf(false) }
|
||||||
var appMenuTarget by remember { mutableStateOf<MinimalApp?>(null) }
|
var appUpdateTarget by remember { mutableStateOf<Update?>(null) }
|
||||||
var showNetworkDialog by remember { mutableStateOf(false) }
|
var showNetworkDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
fun handleNavigation(destination: Destination) {
|
fun handleNavigation(destination: Destination) {
|
||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.AppMenu -> appMenuTarget = destination.app
|
is Destination.AppUpdate -> appUpdateTarget = destination.update
|
||||||
else -> onNavigateTo(destination)
|
else -> onNavigateTo(destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,8 +116,15 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
appMenuTarget?.let { app ->
|
appUpdateTarget?.let { app ->
|
||||||
AppMenuSheet(app = app, onDismiss = { appMenuTarget = null })
|
AppUpdateSheet(
|
||||||
|
update = app,
|
||||||
|
onDismiss = { appUpdateTarget = null },
|
||||||
|
onNavigateTo = { destination ->
|
||||||
|
appUpdateTarget = null
|
||||||
|
onNavigateTo(destination)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -192,7 +199,7 @@ fun MainScreen(
|
|||||||
when (MainTab.entries[page]) {
|
when (MainTab.entries[page]) {
|
||||||
MainTab.APPS -> AppsGamesScreen(
|
MainTab.APPS -> AppsGamesScreen(
|
||||||
pageType = 0,
|
pageType = 0,
|
||||||
onNavigateTo = ::handleNavigation
|
onNavigateTo = onNavigateTo
|
||||||
)
|
)
|
||||||
MainTab.GAMES -> AppsGamesScreen(
|
MainTab.GAMES -> AppsGamesScreen(
|
||||||
pageType = 1,
|
pageType = 1,
|
||||||
@@ -214,10 +221,10 @@ fun MainScreen(
|
|||||||
updatesViewModel.download(update)
|
updatesViewModel.download(update)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequestUpdateAll = {
|
onRequestUpdateAll = { selectedUpdates ->
|
||||||
val needsObb = updatesViewModel.updates.value?.any {
|
val needsObb = selectedUpdates.any {
|
||||||
it.fileList.requiresObbDir()
|
it.fileList.requiresObbDir()
|
||||||
} == true
|
}
|
||||||
if (needsObb && !isGranted(context, PermissionType.STORAGE_MANAGER)) {
|
if (needsObb && !isGranted(context, PermissionType.STORAGE_MANAGER)) {
|
||||||
onNavigateTo(
|
onNavigateTo(
|
||||||
Destination.PermissionRationale(
|
Destination.PermissionRationale(
|
||||||
@@ -225,7 +232,7 @@ fun MainScreen(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
updatesViewModel.downloadAll()
|
updatesViewModel.downloadAll(selectedUpdates)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCancelUpdate = { packageName ->
|
onCancelUpdate = { packageName ->
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.compose.ui.sheets
|
|
||||||
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.NavigationDrawerItem
|
|
||||||
import androidx.compose.material3.NavigationDrawerItemDefaults
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|
||||||
import com.aurora.extensions.openInfo
|
|
||||||
import com.aurora.extensions.toast
|
|
||||||
import com.aurora.store.AuroraApp
|
|
||||||
import com.aurora.store.R
|
|
||||||
import com.aurora.store.data.event.BusEvent
|
|
||||||
import com.aurora.store.data.installer.AppInstaller
|
|
||||||
import com.aurora.store.data.model.MinimalApp
|
|
||||||
import com.aurora.store.util.PackageUtil
|
|
||||||
import com.aurora.store.viewmodel.sheets.AppMenuViewModel
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun AppMenuSheet(
|
|
||||||
app: MinimalApp,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
viewModel: AppMenuViewModel = hiltViewModel()
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val isBlacklisted = viewModel.blacklistProvider.isBlacklisted(app.packageName)
|
|
||||||
val isInstalled = PackageUtil.isInstalled(context, app.packageName)
|
|
||||||
|
|
||||||
val exportLauncher = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.CreateDocument("application/zip")
|
|
||||||
) { uri ->
|
|
||||||
if (uri != null) {
|
|
||||||
viewModel.copyInstalledApp(context, app, uri)
|
|
||||||
} else {
|
|
||||||
context.toast(R.string.failed_apk_export)
|
|
||||||
}
|
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
) {
|
|
||||||
NavigationDrawerItem(
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(
|
|
||||||
if (isBlacklisted) R.drawable.ic_cancel else R.drawable.ic_blacklist
|
|
||||||
),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = {
|
|
||||||
val labelRes = if (isBlacklisted) {
|
|
||||||
R.string.action_whitelist
|
|
||||||
} else {
|
|
||||||
R.string.action_blacklist_add
|
|
||||||
}
|
|
||||||
Text(stringResource(labelRes))
|
|
||||||
},
|
|
||||||
selected = false,
|
|
||||||
onClick = {
|
|
||||||
if (isBlacklisted) {
|
|
||||||
viewModel.blacklistProvider.whitelist(app.packageName)
|
|
||||||
context.toast(R.string.toast_apk_whitelisted)
|
|
||||||
} else {
|
|
||||||
viewModel.blacklistProvider.blacklist(app.packageName)
|
|
||||||
context.toast(R.string.toast_apk_blacklisted)
|
|
||||||
}
|
|
||||||
AuroraApp.events.send(BusEvent.Blacklisted(app.packageName))
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
colors = NavigationDrawerItemDefaults.colors(
|
|
||||||
unselectedContainerColor = androidx.compose.ui.graphics.Color.Transparent
|
|
||||||
)
|
|
||||||
)
|
|
||||||
HorizontalDivider()
|
|
||||||
if (isInstalled) {
|
|
||||||
NavigationDrawerItem(
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_delete_forever),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.action_uninstall)) },
|
|
||||||
selected = false,
|
|
||||||
onClick = {
|
|
||||||
AppInstaller.uninstall(context, app.packageName)
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
colors = NavigationDrawerItemDefaults.colors(
|
|
||||||
unselectedContainerColor = androidx.compose.ui.graphics.Color.Transparent
|
|
||||||
)
|
|
||||||
)
|
|
||||||
NavigationDrawerItem(
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_file_copy),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.action_export)) },
|
|
||||||
selected = false,
|
|
||||||
onClick = { exportLauncher.launch("${app.packageName}.zip") },
|
|
||||||
colors = NavigationDrawerItemDefaults.colors(
|
|
||||||
unselectedContainerColor = androidx.compose.ui.graphics.Color.Transparent
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
NavigationDrawerItem(
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_about),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.action_info)) },
|
|
||||||
selected = false,
|
|
||||||
onClick = {
|
|
||||||
context.openInfo(app.packageName)
|
|
||||||
onDismiss()
|
|
||||||
},
|
|
||||||
colors = NavigationDrawerItemDefaults.colors(
|
|
||||||
unselectedContainerColor = androidx.compose.ui.graphics.Color.Transparent
|
|
||||||
)
|
|
||||||
)
|
|
||||||
HorizontalDivider()
|
|
||||||
androidx.compose.foundation.layout.Spacer(Modifier.navigationBarsPadding())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.compose.ui.sheets
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.dimensionResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.fromHtml
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import com.aurora.extensions.openInfo
|
||||||
|
import com.aurora.extensions.toast
|
||||||
|
import com.aurora.store.AuroraApp
|
||||||
|
import com.aurora.store.R
|
||||||
|
import com.aurora.store.compose.navigation.Destination
|
||||||
|
import com.aurora.store.data.event.BusEvent
|
||||||
|
import com.aurora.store.data.installer.AppInstaller
|
||||||
|
import com.aurora.store.data.room.update.Update
|
||||||
|
import com.aurora.store.viewmodel.sheets.AppUpdateViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AppUpdateSheet(
|
||||||
|
update: Update,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onNavigateTo: (Destination) -> Unit = {},
|
||||||
|
viewModel: AppUpdateViewModel = hiltViewModel()
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val isBlacklisted = viewModel.blacklistProvider.isBlacklisted(update.packageName)
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
AppHeader(
|
||||||
|
update = update,
|
||||||
|
onShowDetails = {
|
||||||
|
onNavigateTo(Destination.AppDetails(update.packageName))
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (update.changelog.isNotEmpty()) {
|
||||||
|
ChangelogSection(html = update.changelog)
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
vertical = dimensionResource(R.dimen.padding_small)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Item(
|
||||||
|
label = stringResource(R.string.action_ignore_all),
|
||||||
|
onClick = {
|
||||||
|
viewModel.ignoreAllUpdates(update.packageName)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Item(
|
||||||
|
label = stringResource(
|
||||||
|
R.string.action_ignore_version,
|
||||||
|
update.versionName
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
viewModel.ignoreVersion(update.packageName, update.versionCode)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
vertical = dimensionResource(R.dimen.padding_small)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Item(
|
||||||
|
label = stringResource(
|
||||||
|
if (isBlacklisted) {
|
||||||
|
R.string.action_whitelist
|
||||||
|
} else {
|
||||||
|
R.string.action_blacklist_add
|
||||||
|
}
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
if (isBlacklisted) {
|
||||||
|
viewModel.blacklistProvider.whitelist(update.packageName)
|
||||||
|
context.toast(R.string.toast_apk_whitelisted)
|
||||||
|
} else {
|
||||||
|
viewModel.blacklistProvider.blacklist(update.packageName)
|
||||||
|
context.toast(R.string.toast_apk_blacklisted)
|
||||||
|
}
|
||||||
|
AuroraApp.events.send(BusEvent.Blacklisted(update.packageName))
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Item(
|
||||||
|
label = stringResource(R.string.action_uninstall),
|
||||||
|
onClick = {
|
||||||
|
AppInstaller.uninstall(context, update.packageName)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Item(
|
||||||
|
label = stringResource(R.string.action_info),
|
||||||
|
onClick = {
|
||||||
|
context.openInfo(update.packageName)
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AppHeader(update: Update, onShowDetails: () -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||||
|
vertical = dimensionResource(R.dimen.padding_xsmall)
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.margin_medium))
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(update.iconURL)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredSize(dimensionResource(R.dimen.icon_size_medium))
|
||||||
|
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_medium)))
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = update.displayName,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = update.packageName,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.version, update.versionName, update.versionCode),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FilledTonalButton(onClick = onShowDetails) {
|
||||||
|
Text(stringResource(R.string.updates_app_details))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Item(label: String, onClick: () -> Unit) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = dimensionResource(R.dimen.padding_xsmall))
|
||||||
|
.clip(RoundedCornerShape(dimensionResource(R.dimen.radius_normal)))
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||||
|
vertical = dimensionResource(R.dimen.padding_small)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChangelogSection(html: String) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.details_changelog),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||||
|
vertical = dimensionResource(R.dimen.padding_xsmall)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = AnnotatedString.fromHtml(html),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||||
|
vertical = dimensionResource(R.dimen.padding_xsmall)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,12 +8,14 @@ package com.aurora.store.compose.ui.updates
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.dimensionResource
|
import androidx.compose.ui.res.dimensionResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
@@ -24,7 +26,7 @@ import com.aurora.store.compose.composable.SectionHeaderWithAction
|
|||||||
import com.aurora.store.compose.composable.ShimmerUpdateItem
|
import com.aurora.store.compose.composable.ShimmerUpdateItem
|
||||||
import com.aurora.store.compose.composable.app.AppUpdateItem
|
import com.aurora.store.compose.composable.app.AppUpdateItem
|
||||||
import com.aurora.store.compose.navigation.Destination
|
import com.aurora.store.compose.navigation.Destination
|
||||||
import com.aurora.store.data.model.MinimalApp
|
import com.aurora.store.data.room.download.Download
|
||||||
import com.aurora.store.data.room.update.Update
|
import com.aurora.store.data.room.update.Update
|
||||||
import com.aurora.store.viewmodel.all.UpdatesViewModel
|
import com.aurora.store.viewmodel.all.UpdatesViewModel
|
||||||
|
|
||||||
@@ -33,11 +35,13 @@ fun UpdatesScreen(
|
|||||||
viewModel: UpdatesViewModel = hiltViewModel(),
|
viewModel: UpdatesViewModel = hiltViewModel(),
|
||||||
onNavigateTo: (Destination) -> Unit = {},
|
onNavigateTo: (Destination) -> Unit = {},
|
||||||
onRequestUpdate: (Update) -> Unit = {},
|
onRequestUpdate: (Update) -> Unit = {},
|
||||||
onRequestUpdateAll: () -> Unit = {},
|
onRequestUpdateAll: (List<Update>) -> Unit = {},
|
||||||
onCancelUpdate: (String) -> Unit = {},
|
onCancelUpdate: (String) -> Unit = {},
|
||||||
onCancelAll: () -> Unit = {}
|
onCancelAll: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
val updates by viewModel.updates.collectAsStateWithLifecycle()
|
val updates by viewModel.updates.collectAsStateWithLifecycle()
|
||||||
|
val ignoredUpdates by viewModel.ignoredUpdates.collectAsStateWithLifecycle()
|
||||||
val downloads by viewModel.downloadsList.collectAsStateWithLifecycle()
|
val downloads by viewModel.downloadsList.collectAsStateWithLifecycle()
|
||||||
val fetchingUpdates by viewModel.fetchingUpdates.collectAsStateWithLifecycle()
|
val fetchingUpdates by viewModel.fetchingUpdates.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
@@ -49,8 +53,24 @@ fun UpdatesScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val allEnqueued = updateMap?.isNotEmpty() == true &&
|
|
||||||
updateMap.values.all { it?.isRunning == true }
|
val groupedUpdates = remember(updateMap) {
|
||||||
|
val main = mutableListOf<Map.Entry<Update, Download?>>()
|
||||||
|
val approval = mutableListOf<Map.Entry<Update, Download?>>()
|
||||||
|
val incompatible = mutableListOf<Map.Entry<Update, Download?>>()
|
||||||
|
updateMap?.entries?.forEach { entry ->
|
||||||
|
when {
|
||||||
|
entry.key.isIncompatible -> incompatible += entry
|
||||||
|
entry.key.requiresOwnershipTransfer(context) -> approval += entry
|
||||||
|
else -> main += entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Triple(main.toList(), approval.toList(), incompatible.toList())
|
||||||
|
}
|
||||||
|
val (mainEntries, approvalEntries, incompatibleEntries) = groupedUpdates
|
||||||
|
|
||||||
|
val mainAllEnqueued = mainEntries.isNotEmpty() &&
|
||||||
|
mainEntries.all { it.value?.isRunning == true }
|
||||||
|
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -80,48 +100,128 @@ fun UpdatesScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val title = "${updateMap.size} " + stringResource(
|
|
||||||
if (updateMap.size == 1) {
|
|
||||||
R.string.update_available
|
|
||||||
} else {
|
|
||||||
R.string.updates_available
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val actionLabel = stringResource(
|
|
||||||
if (allEnqueued) R.string.action_cancel else R.string.action_update_all
|
|
||||||
)
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
verticalArrangement = Arrangement.spacedBy(
|
verticalArrangement = Arrangement.spacedBy(
|
||||||
dimensionResource(R.dimen.margin_medium)
|
dimensionResource(R.dimen.margin_medium)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
item(key = "header") {
|
if (mainEntries.isNotEmpty()) {
|
||||||
|
item(key = "header_main") {
|
||||||
|
val title = "${mainEntries.size} " + stringResource(
|
||||||
|
if (mainEntries.size == 1) {
|
||||||
|
R.string.update_available
|
||||||
|
} else {
|
||||||
|
R.string.updates_available
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val actionLabel = stringResource(
|
||||||
|
if (mainAllEnqueued) {
|
||||||
|
R.string.action_cancel
|
||||||
|
} else {
|
||||||
|
R.string.action_update_all
|
||||||
|
}
|
||||||
|
)
|
||||||
SectionHeaderWithAction(
|
SectionHeaderWithAction(
|
||||||
title = title,
|
title = title,
|
||||||
action = actionLabel,
|
action = actionLabel,
|
||||||
onAction = {
|
onAction = {
|
||||||
if (allEnqueued) onCancelAll() else onRequestUpdateAll()
|
if (mainAllEnqueued) {
|
||||||
|
onCancelAll()
|
||||||
|
} else {
|
||||||
|
onRequestUpdateAll(mainEntries.map { it.key })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateItems(
|
||||||
|
entries = mainEntries,
|
||||||
|
keyPrefix = "main",
|
||||||
|
onNavigateTo = onNavigateTo,
|
||||||
|
onRequestUpdate = onRequestUpdate,
|
||||||
|
onCancelUpdate = onCancelUpdate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (approvalEntries.isNotEmpty()) {
|
||||||
|
item(key = "header_approval") {
|
||||||
|
SectionHeaderWithAction(
|
||||||
|
title = stringResource(R.string.updates_approval_header),
|
||||||
|
subtitle = stringResource(R.string.updates_approval_desc),
|
||||||
|
action = stringResource(R.string.action_update_all),
|
||||||
|
onAction = {
|
||||||
|
onRequestUpdateAll(approvalEntries.map { it.key })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateItems(
|
||||||
|
entries = approvalEntries,
|
||||||
|
keyPrefix = "approval",
|
||||||
|
onNavigateTo = onNavigateTo,
|
||||||
|
onRequestUpdate = onRequestUpdate,
|
||||||
|
onCancelUpdate = onCancelUpdate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (incompatibleEntries.isNotEmpty()) {
|
||||||
|
item(key = "header_incompatible") {
|
||||||
|
SectionHeaderWithAction(
|
||||||
|
title = stringResource(R.string.updates_incompatible_header),
|
||||||
|
subtitle = stringResource(R.string.updates_incompatible_desc)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateItems(
|
||||||
|
entries = incompatibleEntries,
|
||||||
|
keyPrefix = "incompatible",
|
||||||
|
onNavigateTo = onNavigateTo,
|
||||||
|
onRequestUpdate = onRequestUpdate,
|
||||||
|
onCancelUpdate = onCancelUpdate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoredUpdates.isNotEmpty()) {
|
||||||
|
item(key = "header_ignored") {
|
||||||
|
SectionHeaderWithAction(
|
||||||
|
title = stringResource(R.string.updates_ignored_header),
|
||||||
|
subtitle = stringResource(R.string.updates_ignored_desc)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
items(
|
items(
|
||||||
items = updateMap.entries.toList(),
|
items = ignoredUpdates,
|
||||||
key = { it.key.packageName }
|
key = { "ignored-${it.packageName}" }
|
||||||
) { (update, download) ->
|
) { update ->
|
||||||
AppUpdateItem(
|
AppUpdateItem(
|
||||||
update = update,
|
update = update,
|
||||||
download = download,
|
onClick = {},
|
||||||
onClick = { onNavigateTo(Destination.AppDetails(update.packageName)) },
|
onUnignore = { viewModel.unignore(update.packageName) }
|
||||||
onLongClick = {
|
|
||||||
onNavigateTo(Destination.AppMenu(MinimalApp.fromUpdate(update)))
|
|
||||||
},
|
|
||||||
onUpdate = { onRequestUpdate(update) },
|
|
||||||
onCancel = { onCancelUpdate(update.packageName) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.updateItems(
|
||||||
|
entries: List<Map.Entry<Update, Download?>>,
|
||||||
|
keyPrefix: String,
|
||||||
|
onNavigateTo: (Destination) -> Unit,
|
||||||
|
onRequestUpdate: (Update) -> Unit,
|
||||||
|
onCancelUpdate: (String) -> Unit
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = entries,
|
||||||
|
key = { "$keyPrefix-${it.key.packageName}" }
|
||||||
|
) { (update, download) ->
|
||||||
|
AppUpdateItem(
|
||||||
|
update = update,
|
||||||
|
download = download,
|
||||||
|
onClick = {
|
||||||
|
onNavigateTo(Destination.AppUpdate(update))
|
||||||
|
},
|
||||||
|
onUpdate = { onRequestUpdate(update) },
|
||||||
|
onCancel = { onCancelUpdate(update.packageName) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,12 @@ class DownloadHelper @Inject constructor(
|
|||||||
private const val VERSION_CODE = "VERSION_CODE"
|
private const val VERSION_CODE = "VERSION_CODE"
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadsList get() = downloadDao.downloads()
|
// Single stable StateFlow shared across consumers. Previously a `get()` accessor that
|
||||||
|
// returned a new StateFlow on every read, which made `collectAsStateWithLifecycle` re-
|
||||||
|
// collect on every recomposition and briefly replay the `emptyList()` initial value —
|
||||||
|
// causing the Updates screen's per-item button to flicker between "Update" and "Cancel"
|
||||||
|
// on every download tick.
|
||||||
|
val downloadsList = downloadDao.downloads()
|
||||||
.stateIn(AuroraApp.scope, SharingStarted.WhileSubscribed(), emptyList())
|
.stateIn(AuroraApp.scope, SharingStarted.WhileSubscribed(), emptyList())
|
||||||
|
|
||||||
val pagedDownloads get() = downloadDao.pagedDownloads()
|
val pagedDownloads get() = downloadDao.pagedDownloads()
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import com.aurora.store.AuroraApp
|
|||||||
import com.aurora.store.data.event.BusEvent
|
import com.aurora.store.data.event.BusEvent
|
||||||
import com.aurora.store.data.event.InstallerEvent
|
import com.aurora.store.data.event.InstallerEvent
|
||||||
import com.aurora.store.data.model.UpdateMode
|
import com.aurora.store.data.model.UpdateMode
|
||||||
|
import com.aurora.store.data.room.update.IgnoredUpdate
|
||||||
|
import com.aurora.store.data.room.update.IgnoredUpdateDao
|
||||||
|
import com.aurora.store.data.room.update.Update
|
||||||
import com.aurora.store.data.room.update.UpdateDao
|
import com.aurora.store.data.room.update.UpdateDao
|
||||||
import com.aurora.store.data.work.UpdateWorker
|
import com.aurora.store.data.work.UpdateWorker
|
||||||
import com.aurora.store.util.Preferences
|
import com.aurora.store.util.Preferences
|
||||||
@@ -30,6 +33,7 @@ import java.util.concurrent.TimeUnit.HOURS
|
|||||||
import java.util.concurrent.TimeUnit.MINUTES
|
import java.util.concurrent.TimeUnit.MINUTES
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -42,6 +46,7 @@ import kotlinx.coroutines.launch
|
|||||||
*/
|
*/
|
||||||
class UpdateHelper @Inject constructor(
|
class UpdateHelper @Inject constructor(
|
||||||
private val updateDao: UpdateDao,
|
private val updateDao: UpdateDao,
|
||||||
|
private val ignoredUpdateDao: IgnoredUpdateDao,
|
||||||
@ApplicationContext private val context: Context
|
@ApplicationContext private val context: Context
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -86,9 +91,36 @@ class UpdateHelper @Inject constructor(
|
|||||||
private val isExtendedUpdateEnabled
|
private val isExtendedUpdateEnabled
|
||||||
get() = Preferences.getBoolean(context, Preferences.PREFERENCE_UPDATES_EXTENDED)
|
get() = Preferences.getBoolean(context, Preferences.PREFERENCE_UPDATES_EXTENDED)
|
||||||
|
|
||||||
val updates = updateDao.updates()
|
private val filteredUpdates = updateDao.updates()
|
||||||
.map { list -> if (!isExtendedUpdateEnabled) list.filter { it.hasValidCert } else list }
|
.map { list -> if (!isExtendedUpdateEnabled) list.filter { it.hasValidCert } else list }
|
||||||
.stateIn(AuroraApp.scope, SharingStarted.WhileSubscribed(), null)
|
|
||||||
|
/**
|
||||||
|
* Updates surfaced to the UI / notifications. Updates muted by the user via
|
||||||
|
* [ignoreAll] or [ignoreVersion] are filtered out — see [ignoredUpdates] for the
|
||||||
|
* complementary list.
|
||||||
|
*/
|
||||||
|
val updates = combine(filteredUpdates, ignoredUpdateDao.ignoredUpdates()) { list, ignored ->
|
||||||
|
val byPkg = ignored.associateBy { it.packageName }
|
||||||
|
list.filterNot { it.isIgnoredBy(byPkg[it.packageName]) }
|
||||||
|
}.stateIn(AuroraApp.scope, SharingStarted.WhileSubscribed(), null)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates that the user has chosen to ignore (all-future or version-specific) and
|
||||||
|
* which currently have a row in the `update` table. Used to render the "Ignored"
|
||||||
|
* footer section on the Updates screen.
|
||||||
|
*/
|
||||||
|
val ignoredUpdates = combine(
|
||||||
|
filteredUpdates,
|
||||||
|
ignoredUpdateDao.ignoredUpdates()
|
||||||
|
) { list, ignored ->
|
||||||
|
val byPkg = ignored.associateBy { it.packageName }
|
||||||
|
list.filter { it.isIgnoredBy(byPkg[it.packageName]) }
|
||||||
|
}.stateIn(AuroraApp.scope, SharingStarted.WhileSubscribed(), emptyList())
|
||||||
|
|
||||||
|
private fun Update.isIgnoredBy(rule: IgnoredUpdate?): Boolean {
|
||||||
|
if (rule == null) return false
|
||||||
|
return rule.ignoredVersionCode == null || rule.ignoredVersionCode == versionCode
|
||||||
|
}
|
||||||
|
|
||||||
val isCheckingUpdates = WorkManager.getInstance(context)
|
val isCheckingUpdates = WorkManager.getInstance(context)
|
||||||
.getWorkInfosForUniqueWorkFlow(EXPEDITED_UPDATE_WORKER)
|
.getWorkInfosForUniqueWorkFlow(EXPEDITED_UPDATE_WORKER)
|
||||||
@@ -153,6 +185,29 @@ class UpdateHelper @Inject constructor(
|
|||||||
updateDao.deleteAll()
|
updateDao.deleteAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide all future updates for [packageName] until [unignore] is called.
|
||||||
|
*/
|
||||||
|
suspend fun ignoreAll(packageName: String) {
|
||||||
|
ignoredUpdateDao.upsert(IgnoredUpdate(packageName = packageName))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide only [versionCode] for [packageName]; newer versions show up again.
|
||||||
|
*/
|
||||||
|
suspend fun ignoreVersion(packageName: String, versionCode: Long) {
|
||||||
|
ignoredUpdateDao.upsert(
|
||||||
|
IgnoredUpdate(packageName = packageName, ignoredVersionCode = versionCode)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop the ignore rule for [packageName], regardless of which kind it was.
|
||||||
|
*/
|
||||||
|
suspend fun unignore(packageName: String) {
|
||||||
|
ignoredUpdateDao.delete(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancels the automated updates check
|
* Cancels the automated updates check
|
||||||
* @see [UpdateWorker]
|
* @see [UpdateWorker]
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package com.aurora.store.data.model
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.os.Parcelable
|
|
||||||
import com.aurora.gplayapi.data.models.App
|
|
||||||
import com.aurora.store.data.room.update.Update
|
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class MinimalApp(
|
|
||||||
val packageName: String,
|
|
||||||
val versionName: String,
|
|
||||||
val versionCode: Long,
|
|
||||||
val displayName: String,
|
|
||||||
@IgnoredOnParcel
|
|
||||||
val icon: Bitmap? = null
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
fun fromApp(app: App): MinimalApp = MinimalApp(
|
|
||||||
app.packageName,
|
|
||||||
app.versionName,
|
|
||||||
app.versionCode,
|
|
||||||
app.displayName
|
|
||||||
)
|
|
||||||
|
|
||||||
fun fromUpdate(update: Update): MinimalApp = MinimalApp(
|
|
||||||
update.packageName,
|
|
||||||
update.versionName,
|
|
||||||
update.versionCode,
|
|
||||||
update.displayName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,12 +8,14 @@ import com.aurora.store.data.room.download.DownloadConverter
|
|||||||
import com.aurora.store.data.room.download.DownloadDao
|
import com.aurora.store.data.room.download.DownloadDao
|
||||||
import com.aurora.store.data.room.favourite.Favourite
|
import com.aurora.store.data.room.favourite.Favourite
|
||||||
import com.aurora.store.data.room.favourite.FavouriteDao
|
import com.aurora.store.data.room.favourite.FavouriteDao
|
||||||
|
import com.aurora.store.data.room.update.IgnoredUpdate
|
||||||
|
import com.aurora.store.data.room.update.IgnoredUpdateDao
|
||||||
import com.aurora.store.data.room.update.Update
|
import com.aurora.store.data.room.update.Update
|
||||||
import com.aurora.store.data.room.update.UpdateDao
|
import com.aurora.store.data.room.update.UpdateDao
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Download::class, Favourite::class, Update::class],
|
entities = [Download::class, Favourite::class, Update::class, IgnoredUpdate::class],
|
||||||
version = 6,
|
version = 8,
|
||||||
exportSchema = true
|
exportSchema = true
|
||||||
)
|
)
|
||||||
@TypeConverters(DownloadConverter::class)
|
@TypeConverters(DownloadConverter::class)
|
||||||
@@ -21,4 +23,5 @@ abstract class AuroraDatabase : RoomDatabase() {
|
|||||||
abstract fun downloadDao(): DownloadDao
|
abstract fun downloadDao(): DownloadDao
|
||||||
abstract fun favouriteDao(): FavouriteDao
|
abstract fun favouriteDao(): FavouriteDao
|
||||||
abstract fun updateDao(): UpdateDao
|
abstract fun updateDao(): UpdateDao
|
||||||
|
abstract fun ignoredUpdateDao(): IgnoredUpdateDao
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ object MigrationHelper {
|
|||||||
override fun migrate(db: SupportSQLiteDatabase) = migrateFrom5To6(db)
|
override fun migrate(db: SupportSQLiteDatabase) = migrateFrom5To6(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MIGRATION_6_7 = object : Migration(6, 7) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) = migrateFrom6To7(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
val MIGRATION_7_8 = object : Migration(7, 8) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) = migrateFrom7To8(db)
|
||||||
|
}
|
||||||
|
|
||||||
private const val TAG = "MigrationHelper"
|
private const val TAG = "MigrationHelper"
|
||||||
|
|
||||||
private fun migrateFrom1To2(database: SupportSQLiteDatabase) {
|
private fun migrateFrom1To2(database: SupportSQLiteDatabase) {
|
||||||
@@ -112,4 +120,43 @@ object MigrationHelper {
|
|||||||
database.endTransaction()
|
database.endTransaction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add isIncompatible column to update table for flagging updates that cannot be applied
|
||||||
|
* on the current OS (e.g. system app updates on HyperOS / GrapheneOS).
|
||||||
|
*/
|
||||||
|
private fun migrateFrom6To7(database: SupportSQLiteDatabase) {
|
||||||
|
database.beginTransaction()
|
||||||
|
try {
|
||||||
|
database.execSQL(
|
||||||
|
"ALTER TABLE `update` ADD COLUMN isIncompatible INTEGER NOT NULL DEFAULT 0"
|
||||||
|
)
|
||||||
|
database.setTransactionSuccessful()
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.e(TAG, "Failed while migrating from database version 6 to 7", exception)
|
||||||
|
} finally {
|
||||||
|
database.endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add ignored_update table for tracking updates that have been muted by the user.
|
||||||
|
* A null ignoredVersionCode means all future updates for this package are hidden.
|
||||||
|
*/
|
||||||
|
private fun migrateFrom7To8(database: SupportSQLiteDatabase) {
|
||||||
|
database.beginTransaction()
|
||||||
|
try {
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `ignored_update` (" +
|
||||||
|
"`packageName` TEXT NOT NULL, " +
|
||||||
|
"`ignoredVersionCode` INTEGER, " +
|
||||||
|
"PRIMARY KEY(`packageName`))"
|
||||||
|
)
|
||||||
|
database.setTransactionSuccessful()
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.e(TAG, "Failed while migrating from database version 7 to 8", exception)
|
||||||
|
} finally {
|
||||||
|
database.endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import com.aurora.store.data.room.MigrationHelper.MIGRATION_2_3
|
|||||||
import com.aurora.store.data.room.MigrationHelper.MIGRATION_3_4
|
import com.aurora.store.data.room.MigrationHelper.MIGRATION_3_4
|
||||||
import com.aurora.store.data.room.MigrationHelper.MIGRATION_4_5
|
import com.aurora.store.data.room.MigrationHelper.MIGRATION_4_5
|
||||||
import com.aurora.store.data.room.MigrationHelper.MIGRATION_5_6
|
import com.aurora.store.data.room.MigrationHelper.MIGRATION_5_6
|
||||||
|
import com.aurora.store.data.room.MigrationHelper.MIGRATION_6_7
|
||||||
|
import com.aurora.store.data.room.MigrationHelper.MIGRATION_7_8
|
||||||
import com.aurora.store.data.room.download.DownloadConverter
|
import com.aurora.store.data.room.download.DownloadConverter
|
||||||
import com.aurora.store.data.room.download.DownloadDao
|
import com.aurora.store.data.room.download.DownloadDao
|
||||||
import com.aurora.store.data.room.favourite.FavouriteDao
|
import com.aurora.store.data.room.favourite.FavouriteDao
|
||||||
|
import com.aurora.store.data.room.update.IgnoredUpdateDao
|
||||||
import com.aurora.store.data.room.update.UpdateDao
|
import com.aurora.store.data.room.update.UpdateDao
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
@@ -35,7 +38,9 @@ object RoomModule {
|
|||||||
MIGRATION_2_3,
|
MIGRATION_2_3,
|
||||||
MIGRATION_3_4,
|
MIGRATION_3_4,
|
||||||
MIGRATION_4_5,
|
MIGRATION_4_5,
|
||||||
MIGRATION_5_6
|
MIGRATION_5_6,
|
||||||
|
MIGRATION_6_7,
|
||||||
|
MIGRATION_7_8
|
||||||
)
|
)
|
||||||
.addTypeConverter(downloadConverter)
|
.addTypeConverter(downloadConverter)
|
||||||
.build()
|
.build()
|
||||||
@@ -50,4 +55,8 @@ object RoomModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun providesUpdateDao(auroraDatabase: AuroraDatabase): UpdateDao = auroraDatabase.updateDao()
|
fun providesUpdateDao(auroraDatabase: AuroraDatabase): UpdateDao = auroraDatabase.updateDao()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun providesIgnoredUpdateDao(auroraDatabase: AuroraDatabase): IgnoredUpdateDao =
|
||||||
|
auroraDatabase.ignoredUpdateDao()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.data.room.update
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks an update as ignored by the user.
|
||||||
|
*
|
||||||
|
* @property packageName Package whose updates are being ignored.
|
||||||
|
* @property ignoredVersionCode When `null`, all future updates for this package are hidden.
|
||||||
|
* When set, only that specific version is hidden — a newer version arriving will show up
|
||||||
|
* in the Updates list again.
|
||||||
|
*/
|
||||||
|
@Entity(tableName = "ignored_update")
|
||||||
|
data class IgnoredUpdate(
|
||||||
|
@PrimaryKey
|
||||||
|
val packageName: String,
|
||||||
|
val ignoredVersionCode: Long? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.data.room.update
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Upsert
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface IgnoredUpdateDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM ignored_update")
|
||||||
|
fun ignoredUpdates(): Flow<List<IgnoredUpdate>>
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
suspend fun upsert(ignoredUpdate: IgnoredUpdate)
|
||||||
|
|
||||||
|
@Query("DELETE FROM ignored_update WHERE packageName = :packageName")
|
||||||
|
suspend fun delete(packageName: String)
|
||||||
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.aurora.store.data.room.update
|
package com.aurora.store.data.room.update
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.gplayapi.data.models.PlayFile
|
import com.aurora.gplayapi.data.models.PlayFile
|
||||||
|
import com.aurora.store.BuildConfig
|
||||||
import com.aurora.store.data.room.download.SharedLib
|
import com.aurora.store.data.room.download.SharedLib
|
||||||
import com.aurora.store.util.CertUtil
|
import com.aurora.store.util.CertUtil
|
||||||
import com.aurora.store.util.PackageUtil
|
import com.aurora.store.util.PackageUtil
|
||||||
@@ -29,11 +33,12 @@ data class Update(
|
|||||||
val offerType: Int,
|
val offerType: Int,
|
||||||
var fileList: List<PlayFile>,
|
var fileList: List<PlayFile>,
|
||||||
val sharedLibs: List<SharedLib>,
|
val sharedLibs: List<SharedLib>,
|
||||||
val targetSdk: Int = 1
|
val targetSdk: Int = 1,
|
||||||
|
val isIncompatible: Boolean = false
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromApp(context: Context, app: App): Update = Update(
|
fun fromApp(context: Context, app: App, isIncompatible: Boolean = false): Update = Update(
|
||||||
app.packageName,
|
app.packageName,
|
||||||
app.versionCode,
|
app.versionCode,
|
||||||
app.versionName,
|
app.versionName,
|
||||||
@@ -53,7 +58,8 @@ data class Update(
|
|||||||
app.offerType,
|
app.offerType,
|
||||||
app.fileList.filterNot { it.url.isBlank() },
|
app.fileList.filterNot { it.url.isBlank() },
|
||||||
app.dependencies.dependentLibraries.map { SharedLib.fromApp(it) },
|
app.dependencies.dependentLibraries.map { SharedLib.fromApp(it) },
|
||||||
app.targetSdk
|
app.targetSdk,
|
||||||
|
isIncompatible
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,4 +69,27 @@ data class Update(
|
|||||||
|
|
||||||
fun isUpToDate(context: Context): Boolean =
|
fun isUpToDate(context: Context): Boolean =
|
||||||
PackageUtil.isInstalled(context, packageName, versionCode)
|
PackageUtil.isInstalled(context, packageName, versionCode)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update ownership was introduced in Android 14 (API 34) via
|
||||||
|
* [android.content.pm.PackageInstaller.SessionParams.setRequestUpdateOwnership]; if a
|
||||||
|
* different installer holds it, the OS shows a transfer-approval dialog at install time.
|
||||||
|
* Below API 34 the concept does not exist, so this returns false. We also return false when
|
||||||
|
* no installer has claimed ownership (`updateOwnerPackageName == null`) since no transfer
|
||||||
|
* prompt is shown in that case.
|
||||||
|
*/
|
||||||
|
fun requiresOwnershipTransfer(context: Context): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return false
|
||||||
|
if (isSelfUpdate(context)) return false
|
||||||
|
return updateOwnerOnApi34(context.packageManager)?.let {
|
||||||
|
it != BuildConfig.APPLICATION_ID
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
private fun updateOwnerOnApi34(packageManager: PackageManager): String? = try {
|
||||||
|
packageManager.getInstallSourceInfo(packageName).updateOwnerPackageName
|
||||||
|
} catch (_: PackageManager.NameNotFoundException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package com.aurora.store.data.work
|
|||||||
|
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
@@ -20,10 +19,8 @@ import androidx.work.OneTimeWorkRequestBuilder
|
|||||||
import androidx.work.OutOfQuotaPolicy
|
import androidx.work.OutOfQuotaPolicy
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.aurora.store.data.model.MinimalApp
|
|
||||||
import com.aurora.store.data.room.download.Download
|
import com.aurora.store.data.room.download.Download
|
||||||
import com.aurora.store.util.NotificationUtil
|
import com.aurora.store.util.NotificationUtil
|
||||||
import com.aurora.store.util.PackageUtil.getPackageInfo
|
|
||||||
import com.aurora.store.util.PathUtil
|
import com.aurora.store.util.PathUtil
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
@@ -32,7 +29,7 @@ import java.util.zip.ZipEntry
|
|||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An expedited worker to export downloaded or installed apps.
|
* An expedited worker to export downloaded apps to a user-chosen URI as a zip bundle.
|
||||||
*/
|
*/
|
||||||
@HiltWorker
|
@HiltWorker
|
||||||
class ExportWorker @AssistedInject constructor(
|
class ExportWorker @AssistedInject constructor(
|
||||||
@@ -44,7 +41,6 @@ class ExportWorker @AssistedInject constructor(
|
|||||||
private const val TAG = "ExportWorker"
|
private const val TAG = "ExportWorker"
|
||||||
|
|
||||||
private const val URI = "URI"
|
private const val URI = "URI"
|
||||||
private const val IS_DOWNLOAD = "IS_DOWNLOAD"
|
|
||||||
private const val PACKAGE_NAME = "PACKAGE_NAME"
|
private const val PACKAGE_NAME = "PACKAGE_NAME"
|
||||||
private const val VERSION_CODE = "VERSION_CODE"
|
private const val VERSION_CODE = "VERSION_CODE"
|
||||||
private const val DISPLAY_NAME = "DISPLAY_NAME"
|
private const val DISPLAY_NAME = "DISPLAY_NAME"
|
||||||
@@ -52,28 +48,6 @@ class ExportWorker @AssistedInject constructor(
|
|||||||
private const val NOTIFICATION_ID = 500
|
private const val NOTIFICATION_ID = 500
|
||||||
private const val NOTIFICATION_ID_FGS = 501
|
private const val NOTIFICATION_ID_FGS = 501
|
||||||
|
|
||||||
/**
|
|
||||||
* Exports the installed package to the given URI
|
|
||||||
* @param app App to export
|
|
||||||
* @see [ExportWorker]
|
|
||||||
*/
|
|
||||||
fun exportInstalledApp(context: Context, app: MinimalApp, uri: Uri) {
|
|
||||||
val inputData = Data.Builder()
|
|
||||||
.putBoolean(IS_DOWNLOAD, false)
|
|
||||||
.putString(URI, uri.toString())
|
|
||||||
.putString(DISPLAY_NAME, app.displayName)
|
|
||||||
.putString(PACKAGE_NAME, app.packageName)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val oneTimeWorkRequest = OneTimeWorkRequestBuilder<ExportWorker>()
|
|
||||||
.setInputData(inputData)
|
|
||||||
.setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
Log.i(TAG, "Exporting ${app.packageName}")
|
|
||||||
WorkManager.getInstance(context).enqueue(oneTimeWorkRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exports the given download to the URI
|
* Exports the given download to the URI
|
||||||
* @param download Download to export
|
* @param download Download to export
|
||||||
@@ -81,7 +55,6 @@ class ExportWorker @AssistedInject constructor(
|
|||||||
*/
|
*/
|
||||||
fun exportDownloadedApp(context: Context, download: Download, uri: Uri) {
|
fun exportDownloadedApp(context: Context, download: Download, uri: Uri) {
|
||||||
val inputData = Data.Builder()
|
val inputData = Data.Builder()
|
||||||
.putBoolean(IS_DOWNLOAD, download.canInstall(context))
|
|
||||||
.putString(URI, uri.toString())
|
.putString(URI, uri.toString())
|
||||||
.putString(DISPLAY_NAME, download.displayName)
|
.putString(DISPLAY_NAME, download.displayName)
|
||||||
.putString(PACKAGE_NAME, download.packageName)
|
.putString(PACKAGE_NAME, download.packageName)
|
||||||
@@ -101,7 +74,6 @@ class ExportWorker @AssistedInject constructor(
|
|||||||
private lateinit var notificationManager: NotificationManager
|
private lateinit var notificationManager: NotificationManager
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val isDownload = inputData.getBoolean(IS_DOWNLOAD, false)
|
|
||||||
val uri = inputData.getString(URI)!!.toUri()
|
val uri = inputData.getString(URI)!!.toUri()
|
||||||
val packageName = inputData.getString(PACKAGE_NAME)
|
val packageName = inputData.getString(PACKAGE_NAME)
|
||||||
val displayName = inputData.getString(DISPLAY_NAME)
|
val displayName = inputData.getString(DISPLAY_NAME)
|
||||||
@@ -109,18 +81,14 @@ class ExportWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
notificationManager = context.getSystemService<NotificationManager>()!!
|
notificationManager = context.getSystemService<NotificationManager>()!!
|
||||||
|
|
||||||
if (packageName.isNullOrEmpty() || isDownload && versionCode == -1L) {
|
if (packageName.isNullOrEmpty() || versionCode == -1L) {
|
||||||
Log.e(TAG, "Input data is corrupt, bailing out!")
|
Log.e(TAG, "Input data is corrupt, bailing out!")
|
||||||
notifyStatus(displayName ?: String(), uri, false)
|
notifyStatus(displayName ?: String(), uri, false)
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isDownload) {
|
|
||||||
copyDownloadedApp(packageName, versionCode, uri)
|
copyDownloadedApp(packageName, versionCode, uri)
|
||||||
} else {
|
|
||||||
copyInstalledApp(packageName, uri)
|
|
||||||
}
|
|
||||||
notifyStatus(displayName ?: packageName, uri)
|
notifyStatus(displayName ?: packageName, uri)
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.e(TAG, "Failed to export $packageName", exception)
|
Log.e(TAG, "Failed to export $packageName", exception)
|
||||||
@@ -148,18 +116,6 @@ class ExportWorker @AssistedInject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyInstalledApp(packageName: String, uri: Uri) {
|
|
||||||
val packageInfo = getPackageInfo(context, packageName, PackageManager.GET_META_DATA)
|
|
||||||
val fileList: MutableList<File?> = mutableListOf()
|
|
||||||
|
|
||||||
fileList.add(File(packageInfo.applicationInfo!!.sourceDir))
|
|
||||||
packageInfo.applicationInfo!!.splitSourceDirs?.let { splits ->
|
|
||||||
fileList.addAll(splits.map { File(it) })
|
|
||||||
}
|
|
||||||
|
|
||||||
bundleAllAPKs(fileList.filterNotNull(), uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyDownloadedApp(packageName: String, versionCode: Long, uri: Uri) = bundleAllAPKs(
|
private fun copyDownloadedApp(packageName: String, versionCode: Long, uri: Uri) = bundleAllAPKs(
|
||||||
PathUtil.getAppDownloadDir(context, packageName, versionCode).listFiles()!!.toList(),
|
PathUtil.getAppDownloadDir(context, packageName, versionCode).listFiles()!!.toList(),
|
||||||
uri
|
uri
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.aurora.store.data.work
|
|||||||
|
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.hilt.work.HiltWorker
|
import androidx.hilt.work.HiltWorker
|
||||||
@@ -9,6 +10,8 @@ import androidx.work.ForegroundInfo
|
|||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import com.aurora.Constants
|
import com.aurora.Constants
|
||||||
import com.aurora.extensions.TAG
|
import com.aurora.extensions.TAG
|
||||||
|
import com.aurora.extensions.isGrapheneOS
|
||||||
|
import com.aurora.extensions.isHyperOS
|
||||||
import com.aurora.extensions.isIgnoringBatteryOptimizations
|
import com.aurora.extensions.isIgnoringBatteryOptimizations
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.gplayapi.helpers.AppDetailsHelper
|
import com.aurora.gplayapi.helpers.AppDetailsHelper
|
||||||
@@ -99,10 +102,14 @@ class UpdateWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val updates = checkUpdates()
|
val allUpdates = checkUpdates()
|
||||||
.also { updateDao.insertUpdates(it) }
|
.also { updateDao.insertUpdates(it) }
|
||||||
.filter { if (!isExtendedUpdateEnabled) it.hasValidCert else true }
|
.filter { if (!isExtendedUpdateEnabled) it.hasValidCert else true }
|
||||||
|
|
||||||
|
// Incompatible updates (e.g. system app updates on HyperOS / GrapheneOS) are
|
||||||
|
// surfaced in the UI but excluded from notifications and auto-install.
|
||||||
|
val updates = allUpdates.filterNot { it.isIncompatible }
|
||||||
|
|
||||||
if (updates.isEmpty() || updateMode == UpdateMode.CHECK_ONLY) {
|
if (updates.isEmpty() || updateMode == UpdateMode.CHECK_ONLY) {
|
||||||
Log.i(TAG, "Found ${updates.size} updates")
|
Log.i(TAG, "Found ${updates.size} updates")
|
||||||
return Result.success()
|
return Result.success()
|
||||||
@@ -171,6 +178,18 @@ class UpdateWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}.map { it.packageName }
|
}.map { it.packageName }
|
||||||
|
|
||||||
|
// HyperOS and GrapheneOS block third-party updates of pristine system apps.
|
||||||
|
val osBlocksSystemAppUpdates = isHyperOS || isGrapheneOS
|
||||||
|
val pristineSystemPackages: Set<String> = if (osBlocksSystemAppUpdates) {
|
||||||
|
packages.filter {
|
||||||
|
val flags = it.applicationInfo?.flags ?: 0
|
||||||
|
flags and ApplicationInfo.FLAG_SYSTEM != 0 &&
|
||||||
|
flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP == 0
|
||||||
|
}.map { it.packageName }.toSet()
|
||||||
|
} else {
|
||||||
|
emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
val updates = appDetailsHelper.getAppByPackageName(filteredPackages)
|
val updates = appDetailsHelper.getAppByPackageName(filteredPackages)
|
||||||
.filter { it.displayName.isNotEmpty() }
|
.filter { it.displayName.isNotEmpty() }
|
||||||
.filter { PackageUtil.isUpdatable(context, it.packageName, it.versionCode) }
|
.filter { PackageUtil.isUpdatable(context, it.packageName, it.versionCode) }
|
||||||
@@ -178,8 +197,13 @@ class UpdateWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
if (canSelfUpdate) getSelfUpdate()?.let { updates.add(it) }
|
if (canSelfUpdate) getSelfUpdate()?.let { updates.add(it) }
|
||||||
|
|
||||||
return@withContext updates.map { Update.fromApp(context, it) }
|
return@withContext updates.map {
|
||||||
.sortedBy { it.displayName.lowercase(Locale.getDefault()) }
|
Update.fromApp(
|
||||||
|
context,
|
||||||
|
it,
|
||||||
|
isIncompatible = it.packageName in pristineSystemPackages
|
||||||
|
)
|
||||||
|
}.sortedBy { it.displayName.lowercase(Locale.getDefault()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,10 +83,13 @@ object CertUtil {
|
|||||||
|
|
||||||
private fun isSignedByFDroid(context: Context, packageName: String): Boolean = try {
|
private fun isSignedByFDroid(context: Context, packageName: String): Boolean = try {
|
||||||
getX509Certificates(context, packageName).any { cert ->
|
getX509Certificates(context, packageName).any { cert ->
|
||||||
cert.subjectDN.name.split(",").associate {
|
cert.subjectDN.name
|
||||||
val (left, right) = it.split("=")
|
.split(",")
|
||||||
left to right
|
.mapNotNull {
|
||||||
}["O"] == "fdroid.org"
|
val parts = it.split("=", limit = 2)
|
||||||
|
if (parts.size == 2) parts[0] to parts[1] else null
|
||||||
|
}
|
||||||
|
.toMap()["O"] == "fdroid.org"
|
||||||
}
|
}
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.e(TAG, "Failed to check signing cert for $packageName", exception)
|
Log.e(TAG, "Failed to check signing cert for $packageName", exception)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class UpdatesViewModel @Inject constructor(
|
|||||||
|
|
||||||
val downloadsList get() = downloadHelper.downloadsList
|
val downloadsList get() = downloadHelper.downloadsList
|
||||||
val updates get() = updateHelper.updates
|
val updates get() = updateHelper.updates
|
||||||
|
val ignoredUpdates get() = updateHelper.ignoredUpdates
|
||||||
|
|
||||||
val fetchingUpdates = updateHelper.isCheckingUpdates
|
val fetchingUpdates = updateHelper.isCheckingUpdates
|
||||||
|
|
||||||
@@ -45,13 +46,17 @@ class UpdatesViewModel @Inject constructor(
|
|||||||
updateHelper.checkUpdatesNow()
|
updateHelper.checkUpdatesNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun unignore(packageName: String) {
|
||||||
|
viewModelScope.launch { updateHelper.unignore(packageName) }
|
||||||
|
}
|
||||||
|
|
||||||
fun download(update: Update) {
|
fun download(update: Update) {
|
||||||
viewModelScope.launch { downloadHelper.enqueueUpdate(update) }
|
viewModelScope.launch { downloadHelper.enqueueUpdate(update) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun downloadAll() {
|
fun downloadAll(updates: List<Update>) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
updates.value?.forEach { downloadHelper.enqueueUpdate(it) }
|
updates.forEach { downloadHelper.enqueueUpdate(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.aurora.store.viewmodel.sheets
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import com.aurora.store.data.model.MinimalApp
|
|
||||||
import com.aurora.store.data.providers.BlacklistProvider
|
|
||||||
import com.aurora.store.data.work.ExportWorker
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class AppMenuViewModel @Inject constructor(val blacklistProvider: BlacklistProvider) : ViewModel() {
|
|
||||||
|
|
||||||
fun copyInstalledApp(context: Context, app: MinimalApp, uri: Uri) {
|
|
||||||
ExportWorker.exportInstalledApp(context, app, uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.viewmodel.sheets
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.aurora.store.data.helper.UpdateHelper
|
||||||
|
import com.aurora.store.data.providers.BlacklistProvider
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class AppUpdateViewModel @Inject constructor(
|
||||||
|
val blacklistProvider: BlacklistProvider,
|
||||||
|
private val updateHelper: UpdateHelper
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
fun ignoreAllUpdates(packageName: String) {
|
||||||
|
viewModelScope.launch { updateHelper.ignoreAll(packageName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ignoreVersion(packageName: String, versionCode: Long) {
|
||||||
|
viewModelScope.launch { updateHelper.ignoreVersion(packageName, versionCode) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -307,6 +307,16 @@
|
|||||||
<string name="app_updater_service_notif_text">Enables background app download</string>
|
<string name="app_updater_service_notif_text">Enables background app download</string>
|
||||||
<string name="update_available">update available</string>
|
<string name="update_available">update available</string>
|
||||||
<string name="updates_available">updates available</string>
|
<string name="updates_available">updates available</string>
|
||||||
|
<string name="updates_incompatible_header">Incompatible updates</string>
|
||||||
|
<string name="updates_incompatible_desc">These system app updates cannot be installed on this OS</string>
|
||||||
|
<string name="updates_approval_header">Updates requiring approval</string>
|
||||||
|
<string name="updates_approval_desc">These apps were not installed by Aurora. Updating them prompts to transfer app update ownership.</string>
|
||||||
|
<string name="updates_app_details">Details</string>
|
||||||
|
<string name="updates_ignored_header">Ignored</string>
|
||||||
|
<string name="updates_ignored_desc">These updates are hidden until you unignore them</string>
|
||||||
|
<string name="action_ignore_all">Ignore all updates</string>
|
||||||
|
<string name="action_ignore_version">Skip version <xliff:g id="version_name">%1$s</xliff:g></string>
|
||||||
|
<string name="action_unignore">Unignore</string>
|
||||||
<string name="error">An error occurred!</string>
|
<string name="error">An error occurred!</string>
|
||||||
<string name="menu">Menu</string>
|
<string name="menu">Menu</string>
|
||||||
<string name="expand">Expand</string>
|
<string name="expand">Expand</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user