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
|
||||
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
|
||||
get() = Build.MANUFACTURER.lowercase(Locale.getDefault()).contains("huawei") ||
|
||||
Build.HARDWARE.lowercase(Locale.getDefault()).contains("kirin") ||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package com.aurora.store.compose.composable
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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
|
||||
fun SectionHeaderWithAction(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
action: String,
|
||||
action: String? = null,
|
||||
subtitle: String? = null,
|
||||
onAction: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
@@ -87,19 +90,31 @@ fun SectionHeaderWithAction(
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
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) {
|
||||
Text(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewWrapper(ThemePreviewProvider::class)
|
||||
@Preview(showBackground = true)
|
||||
|
||||
@@ -42,7 +42,6 @@ fun StreamCarousel(
|
||||
lazyListState: LazyListState = rememberLazyListState(),
|
||||
onHeaderClick: (StreamCluster) -> Unit = {},
|
||||
onAppClick: (App) -> Unit = {},
|
||||
onAppLongClick: (App) -> Unit = {},
|
||||
onClusterScrolled: (StreamCluster) -> Unit = {},
|
||||
onScrolledToEnd: () -> Unit = {}
|
||||
) {
|
||||
@@ -118,7 +117,6 @@ fun StreamCarousel(
|
||||
ClusterRow(
|
||||
cluster = cluster,
|
||||
onAppClick = onAppClick,
|
||||
onAppLongClick = onAppLongClick,
|
||||
onClusterScrolled = onClusterScrolled
|
||||
)
|
||||
}
|
||||
@@ -135,7 +133,6 @@ fun StreamCarousel(
|
||||
internal fun ClusterRow(
|
||||
cluster: StreamCluster,
|
||||
onAppClick: (App) -> Unit = {},
|
||||
onAppLongClick: (App) -> Unit = {},
|
||||
onClusterScrolled: (StreamCluster) -> Unit = {}
|
||||
) {
|
||||
val rowState = rememberLazyListState()
|
||||
@@ -162,8 +159,7 @@ internal fun ClusterRow(
|
||||
) { _, app ->
|
||||
AppListItem(
|
||||
app = app,
|
||||
onClick = { onAppClick(app) },
|
||||
onLongClick = { onAppLongClick(app) }
|
||||
onClick = { onAppClick(app) }
|
||||
)
|
||||
}
|
||||
if (cluster.hasNext()) {
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
|
||||
package com.aurora.store.compose.composable.app
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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 app [App] to display
|
||||
* @param onClick Callback when the composable is clicked
|
||||
* @param onLongClick Callback when the composable is long-clicked
|
||||
* @see LargeAppListItem
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun AppListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
app: App,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {}
|
||||
) {
|
||||
fun AppListItem(modifier: Modifier = Modifier, app: App, onClick: () -> Unit = {}) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(dimensionResource(R.dimen.icon_size_cluster))
|
||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(all = dimensionResource(R.dimen.padding_xxsmall)),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
|
||||
package com.aurora.store.compose.composable.app
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.requiredSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
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.Modifier
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewWrapper
|
||||
@@ -52,9 +39,9 @@ fun AppUpdateItem(
|
||||
update: Update,
|
||||
download: Download? = null,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {},
|
||||
onUpdate: () -> Unit = {},
|
||||
onCancel: () -> Unit = {}
|
||||
onCancel: () -> Unit = {},
|
||||
onUnignore: (() -> Unit)? = null
|
||||
) {
|
||||
val inProgress = download != null && download.status !in DownloadStatus.finished
|
||||
val progress = if (download?.status == DownloadStatus.DOWNLOADING) {
|
||||
@@ -62,13 +49,11 @@ fun AppUpdateItem(
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
var changelogExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(
|
||||
horizontal = dimensionResource(R.dimen.padding_medium),
|
||||
vertical = dimensionResource(R.dimen.padding_small)
|
||||
@@ -83,7 +68,7 @@ fun AppUpdateItem(
|
||||
progress = progress
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(dimensionResource(R.dimen.margin_small)))
|
||||
Spacer(Modifier.width(dimensionResource(R.dimen.margin_medium)))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = update.displayName,
|
||||
@@ -93,68 +78,39 @@ fun AppUpdateItem(
|
||||
)
|
||||
Text(
|
||||
text = update.developerName,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = "${CommonUtil.addSiPrefix(update.size)} • ${update.updatedOn}",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
Text(
|
||||
text = "${update.versionName} (${update.versionCode})",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
text = "${update.versionName} • ${CommonUtil.addSiPrefix(update.size)}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
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) {
|
||||
Text(stringResource(R.string.action_cancel))
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
else -> {
|
||||
Button(onClick = onUpdate) {
|
||||
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.StreamCluster
|
||||
import com.aurora.store.data.model.MinimalApp
|
||||
import com.aurora.store.data.model.PermissionType
|
||||
import com.aurora.store.data.room.update.Update
|
||||
|
||||
/**
|
||||
* All navigation actions available to composable screens.
|
||||
@@ -20,7 +20,7 @@ sealed class Destination {
|
||||
|
||||
data class AppDetails(val packageName: 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 Downloads : Destination()
|
||||
|
||||
@@ -128,7 +128,7 @@ fun NavDisplay(startDestination: NavKey) {
|
||||
|
||||
is Destination.AppDetails -> backstack.add(Screen.AppDetails(destination.packageName))
|
||||
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))
|
||||
|
||||
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.store.R
|
||||
import com.aurora.store.compose.navigation.Destination
|
||||
import com.aurora.store.data.model.MinimalApp
|
||||
import com.aurora.store.util.Preferences
|
||||
import com.aurora.store.viewmodel.category.CategoryViewModel
|
||||
import com.aurora.store.viewmodel.homestream.StreamViewModel
|
||||
@@ -100,7 +99,6 @@ fun AppsGamesScreen(
|
||||
pageType = pageType,
|
||||
viewModel = streamViewModel,
|
||||
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
|
||||
onAppLongClick = { onNavigateTo(Destination.AppMenu(MinimalApp.fromApp(it))) },
|
||||
onHeaderClick = { onNavigateTo(Destination.StreamBrowse(it)) },
|
||||
onClusterScrolled = { cluster ->
|
||||
streamViewModel.observeCluster(category(pageType), cluster)
|
||||
|
||||
@@ -24,7 +24,6 @@ internal fun ForYouContent(
|
||||
pageType: Int,
|
||||
viewModel: StreamViewModel,
|
||||
onAppClick: (App) -> Unit,
|
||||
onAppLongClick: (App) -> Unit,
|
||||
onHeaderClick: (StreamCluster) -> Unit,
|
||||
onClusterScrolled: (StreamCluster) -> Unit,
|
||||
onScrolledToEnd: () -> Unit
|
||||
@@ -43,7 +42,6 @@ internal fun ForYouContent(
|
||||
streamBundle = streamBundle?.get(category),
|
||||
onHeaderClick = onHeaderClick,
|
||||
onAppClick = onAppClick,
|
||||
onAppLongClick = onAppLongClick,
|
||||
onClusterScrolled = onClusterScrolled,
|
||||
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.commons.MoreSheet
|
||||
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.data.model.MinimalApp
|
||||
import com.aurora.store.data.model.NetworkStatus
|
||||
import com.aurora.store.data.model.PermissionType
|
||||
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 kotlinx.coroutines.launch
|
||||
|
||||
@@ -88,12 +88,12 @@ fun MainScreen(
|
||||
}
|
||||
|
||||
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) }
|
||||
|
||||
fun handleNavigation(destination: Destination) {
|
||||
when (destination) {
|
||||
is Destination.AppMenu -> appMenuTarget = destination.app
|
||||
is Destination.AppUpdate -> appUpdateTarget = destination.update
|
||||
else -> onNavigateTo(destination)
|
||||
}
|
||||
}
|
||||
@@ -116,8 +116,15 @@ fun MainScreen(
|
||||
)
|
||||
}
|
||||
|
||||
appMenuTarget?.let { app ->
|
||||
AppMenuSheet(app = app, onDismiss = { appMenuTarget = null })
|
||||
appUpdateTarget?.let { app ->
|
||||
AppUpdateSheet(
|
||||
update = app,
|
||||
onDismiss = { appUpdateTarget = null },
|
||||
onNavigateTo = { destination ->
|
||||
appUpdateTarget = null
|
||||
onNavigateTo(destination)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -192,7 +199,7 @@ fun MainScreen(
|
||||
when (MainTab.entries[page]) {
|
||||
MainTab.APPS -> AppsGamesScreen(
|
||||
pageType = 0,
|
||||
onNavigateTo = ::handleNavigation
|
||||
onNavigateTo = onNavigateTo
|
||||
)
|
||||
MainTab.GAMES -> AppsGamesScreen(
|
||||
pageType = 1,
|
||||
@@ -214,10 +221,10 @@ fun MainScreen(
|
||||
updatesViewModel.download(update)
|
||||
}
|
||||
},
|
||||
onRequestUpdateAll = {
|
||||
val needsObb = updatesViewModel.updates.value?.any {
|
||||
onRequestUpdateAll = { selectedUpdates ->
|
||||
val needsObb = selectedUpdates.any {
|
||||
it.fileList.requiresObbDir()
|
||||
} == true
|
||||
}
|
||||
if (needsObb && !isGranted(context, PermissionType.STORAGE_MANAGER)) {
|
||||
onNavigateTo(
|
||||
Destination.PermissionRationale(
|
||||
@@ -225,7 +232,7 @@ fun MainScreen(
|
||||
)
|
||||
)
|
||||
} else {
|
||||
updatesViewModel.downloadAll()
|
||||
updatesViewModel.downloadAll(selectedUpdates)
|
||||
}
|
||||
},
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.app.AppUpdateItem
|
||||
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.viewmodel.all.UpdatesViewModel
|
||||
|
||||
@@ -33,11 +35,13 @@ fun UpdatesScreen(
|
||||
viewModel: UpdatesViewModel = hiltViewModel(),
|
||||
onNavigateTo: (Destination) -> Unit = {},
|
||||
onRequestUpdate: (Update) -> Unit = {},
|
||||
onRequestUpdateAll: () -> Unit = {},
|
||||
onRequestUpdateAll: (List<Update>) -> Unit = {},
|
||||
onCancelUpdate: (String) -> Unit = {},
|
||||
onCancelAll: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val updates by viewModel.updates.collectAsStateWithLifecycle()
|
||||
val ignoredUpdates by viewModel.ignoredUpdates.collectAsStateWithLifecycle()
|
||||
val downloads by viewModel.downloadsList.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(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -80,48 +100,128 @@ fun UpdatesScreen(
|
||||
}
|
||||
|
||||
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(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
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(
|
||||
title = title,
|
||||
action = actionLabel,
|
||||
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 = updateMap.entries.toList(),
|
||||
key = { it.key.packageName }
|
||||
items = ignoredUpdates,
|
||||
key = { "ignored-${it.packageName}" }
|
||||
) { update ->
|
||||
AppUpdateItem(
|
||||
update = update,
|
||||
onClick = {},
|
||||
onUnignore = { viewModel.unignore(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.AppDetails(update.packageName)) },
|
||||
onLongClick = {
|
||||
onNavigateTo(Destination.AppMenu(MinimalApp.fromUpdate(update)))
|
||||
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"
|
||||
}
|
||||
|
||||
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())
|
||||
|
||||
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.InstallerEvent
|
||||
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.work.UpdateWorker
|
||||
import com.aurora.store.util.Preferences
|
||||
@@ -30,6 +33,7 @@ import java.util.concurrent.TimeUnit.HOURS
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -42,6 +46,7 @@ import kotlinx.coroutines.launch
|
||||
*/
|
||||
class UpdateHelper @Inject constructor(
|
||||
private val updateDao: UpdateDao,
|
||||
private val ignoredUpdateDao: IgnoredUpdateDao,
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
@@ -86,9 +91,36 @@ class UpdateHelper @Inject constructor(
|
||||
private val isExtendedUpdateEnabled
|
||||
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 }
|
||||
.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)
|
||||
.getWorkInfosForUniqueWorkFlow(EXPEDITED_UPDATE_WORKER)
|
||||
@@ -153,6 +185,29 @@ class UpdateHelper @Inject constructor(
|
||||
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
|
||||
* @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.favourite.Favourite
|
||||
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.UpdateDao
|
||||
|
||||
@Database(
|
||||
entities = [Download::class, Favourite::class, Update::class],
|
||||
version = 6,
|
||||
entities = [Download::class, Favourite::class, Update::class, IgnoredUpdate::class],
|
||||
version = 8,
|
||||
exportSchema = true
|
||||
)
|
||||
@TypeConverters(DownloadConverter::class)
|
||||
@@ -21,4 +23,5 @@ abstract class AuroraDatabase : RoomDatabase() {
|
||||
abstract fun downloadDao(): DownloadDao
|
||||
abstract fun favouriteDao(): FavouriteDao
|
||||
abstract fun updateDao(): UpdateDao
|
||||
abstract fun ignoredUpdateDao(): IgnoredUpdateDao
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ object MigrationHelper {
|
||||
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 fun migrateFrom1To2(database: SupportSQLiteDatabase) {
|
||||
@@ -112,4 +120,43 @@ object MigrationHelper {
|
||||
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_4_5
|
||||
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.DownloadDao
|
||||
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 dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -35,7 +38,9 @@ object RoomModule {
|
||||
MIGRATION_2_3,
|
||||
MIGRATION_3_4,
|
||||
MIGRATION_4_5,
|
||||
MIGRATION_5_6
|
||||
MIGRATION_5_6,
|
||||
MIGRATION_6_7,
|
||||
MIGRATION_7_8
|
||||
)
|
||||
.addTypeConverter(downloadConverter)
|
||||
.build()
|
||||
@@ -50,4 +55,8 @@ object RoomModule {
|
||||
|
||||
@Provides
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
import com.aurora.gplayapi.data.models.PlayFile
|
||||
import com.aurora.store.BuildConfig
|
||||
import com.aurora.store.data.room.download.SharedLib
|
||||
import com.aurora.store.util.CertUtil
|
||||
import com.aurora.store.util.PackageUtil
|
||||
@@ -29,11 +33,12 @@ data class Update(
|
||||
val offerType: Int,
|
||||
var fileList: List<PlayFile>,
|
||||
val sharedLibs: List<SharedLib>,
|
||||
val targetSdk: Int = 1
|
||||
val targetSdk: Int = 1,
|
||||
val isIncompatible: Boolean = false
|
||||
) : Parcelable {
|
||||
|
||||
companion object {
|
||||
fun fromApp(context: Context, app: App): Update = Update(
|
||||
fun fromApp(context: Context, app: App, isIncompatible: Boolean = false): Update = Update(
|
||||
app.packageName,
|
||||
app.versionCode,
|
||||
app.versionName,
|
||||
@@ -53,7 +58,8 @@ data class Update(
|
||||
app.offerType,
|
||||
app.fileList.filterNot { it.url.isBlank() },
|
||||
app.dependencies.dependentLibraries.map { SharedLib.fromApp(it) },
|
||||
app.targetSdk
|
||||
app.targetSdk,
|
||||
isIncompatible
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,4 +69,27 @@ data class Update(
|
||||
|
||||
fun isUpToDate(context: Context): Boolean =
|
||||
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.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
@@ -20,10 +19,8 @@ import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.OutOfQuotaPolicy
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.aurora.store.data.model.MinimalApp
|
||||
import com.aurora.store.data.room.download.Download
|
||||
import com.aurora.store.util.NotificationUtil
|
||||
import com.aurora.store.util.PackageUtil.getPackageInfo
|
||||
import com.aurora.store.util.PathUtil
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
@@ -32,7 +29,7 @@ import java.util.zip.ZipEntry
|
||||
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
|
||||
class ExportWorker @AssistedInject constructor(
|
||||
@@ -44,7 +41,6 @@ class ExportWorker @AssistedInject constructor(
|
||||
private const val TAG = "ExportWorker"
|
||||
|
||||
private const val URI = "URI"
|
||||
private const val IS_DOWNLOAD = "IS_DOWNLOAD"
|
||||
private const val PACKAGE_NAME = "PACKAGE_NAME"
|
||||
private const val VERSION_CODE = "VERSION_CODE"
|
||||
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_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
|
||||
* @param download Download to export
|
||||
@@ -81,7 +55,6 @@ class ExportWorker @AssistedInject constructor(
|
||||
*/
|
||||
fun exportDownloadedApp(context: Context, download: Download, uri: Uri) {
|
||||
val inputData = Data.Builder()
|
||||
.putBoolean(IS_DOWNLOAD, download.canInstall(context))
|
||||
.putString(URI, uri.toString())
|
||||
.putString(DISPLAY_NAME, download.displayName)
|
||||
.putString(PACKAGE_NAME, download.packageName)
|
||||
@@ -101,7 +74,6 @@ class ExportWorker @AssistedInject constructor(
|
||||
private lateinit var notificationManager: NotificationManager
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val isDownload = inputData.getBoolean(IS_DOWNLOAD, false)
|
||||
val uri = inputData.getString(URI)!!.toUri()
|
||||
val packageName = inputData.getString(PACKAGE_NAME)
|
||||
val displayName = inputData.getString(DISPLAY_NAME)
|
||||
@@ -109,18 +81,14 @@ class ExportWorker @AssistedInject constructor(
|
||||
|
||||
notificationManager = context.getSystemService<NotificationManager>()!!
|
||||
|
||||
if (packageName.isNullOrEmpty() || isDownload && versionCode == -1L) {
|
||||
if (packageName.isNullOrEmpty() || versionCode == -1L) {
|
||||
Log.e(TAG, "Input data is corrupt, bailing out!")
|
||||
notifyStatus(displayName ?: String(), uri, false)
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
try {
|
||||
if (isDownload) {
|
||||
copyDownloadedApp(packageName, versionCode, uri)
|
||||
} else {
|
||||
copyInstalledApp(packageName, uri)
|
||||
}
|
||||
notifyStatus(displayName ?: packageName, uri)
|
||||
} catch (exception: 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(
|
||||
PathUtil.getAppDownloadDir(context, packageName, versionCode).listFiles()!!.toList(),
|
||||
uri
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.aurora.store.data.work
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.util.Log
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.hilt.work.HiltWorker
|
||||
@@ -9,6 +10,8 @@ import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import com.aurora.Constants
|
||||
import com.aurora.extensions.TAG
|
||||
import com.aurora.extensions.isGrapheneOS
|
||||
import com.aurora.extensions.isHyperOS
|
||||
import com.aurora.extensions.isIgnoringBatteryOptimizations
|
||||
import com.aurora.gplayapi.data.models.App
|
||||
import com.aurora.gplayapi.helpers.AppDetailsHelper
|
||||
@@ -99,10 +102,14 @@ class UpdateWorker @AssistedInject constructor(
|
||||
}
|
||||
|
||||
try {
|
||||
val updates = checkUpdates()
|
||||
val allUpdates = checkUpdates()
|
||||
.also { updateDao.insertUpdates(it) }
|
||||
.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) {
|
||||
Log.i(TAG, "Found ${updates.size} updates")
|
||||
return Result.success()
|
||||
@@ -171,6 +178,18 @@ class UpdateWorker @AssistedInject constructor(
|
||||
}
|
||||
}.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)
|
||||
.filter { it.displayName.isNotEmpty() }
|
||||
.filter { PackageUtil.isUpdatable(context, it.packageName, it.versionCode) }
|
||||
@@ -178,8 +197,13 @@ class UpdateWorker @AssistedInject constructor(
|
||||
|
||||
if (canSelfUpdate) getSelfUpdate()?.let { updates.add(it) }
|
||||
|
||||
return@withContext updates.map { Update.fromApp(context, it) }
|
||||
.sortedBy { it.displayName.lowercase(Locale.getDefault()) }
|
||||
return@withContext updates.map {
|
||||
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 {
|
||||
getX509Certificates(context, packageName).any { cert ->
|
||||
cert.subjectDN.name.split(",").associate {
|
||||
val (left, right) = it.split("=")
|
||||
left to right
|
||||
}["O"] == "fdroid.org"
|
||||
cert.subjectDN.name
|
||||
.split(",")
|
||||
.mapNotNull {
|
||||
val parts = it.split("=", limit = 2)
|
||||
if (parts.size == 2) parts[0] to parts[1] else null
|
||||
}
|
||||
.toMap()["O"] == "fdroid.org"
|
||||
}
|
||||
} catch (exception: 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 updates get() = updateHelper.updates
|
||||
val ignoredUpdates get() = updateHelper.ignoredUpdates
|
||||
|
||||
val fetchingUpdates = updateHelper.isCheckingUpdates
|
||||
|
||||
@@ -45,13 +46,17 @@ class UpdatesViewModel @Inject constructor(
|
||||
updateHelper.checkUpdatesNow()
|
||||
}
|
||||
|
||||
fun unignore(packageName: String) {
|
||||
viewModelScope.launch { updateHelper.unignore(packageName) }
|
||||
}
|
||||
|
||||
fun download(update: Update) {
|
||||
viewModelScope.launch { downloadHelper.enqueueUpdate(update) }
|
||||
}
|
||||
|
||||
fun downloadAll() {
|
||||
fun downloadAll(updates: List<Update>) {
|
||||
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="update_available">update 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="menu">Menu</string>
|
||||
<string name="expand">Expand</string>
|
||||
|
||||
Reference in New Issue
Block a user