Rename package to com.machaiv3lli.fdroid

This commit is contained in:
machiav3lli
2022-07-05 03:21:35 +02:00
parent b8deae87ea
commit c482580686
126 changed files with 689 additions and 684 deletions

View File

@ -0,0 +1,101 @@
package com.machiav3lli.fdroid.database
import androidx.room.TypeConverter
import com.machiav3lli.fdroid.database.entity.Release
import com.machiav3lli.fdroid.entity.Author
import com.machiav3lli.fdroid.entity.Donate
import com.machiav3lli.fdroid.entity.Screenshot
object Converters {
@TypeConverter
@JvmStatic
fun toStringList(byteArray: ByteArray): List<String> {
val string = String(byteArray)
return if (string == "") emptyList()
else string.removeSurrounding("[", "]").split(", ").filter(String::isNotEmpty)
}
@JvmName("stringListToByteArray")
@TypeConverter
@JvmStatic
fun toByteArray(list: List<String>): ByteArray = list.toString().toByteArray()
@TypeConverter
@JvmStatic
fun toPairStringList(byteArray: ByteArray): List<Pair<String, String>> {
val string = String(byteArray)
return if (string == "") emptyList()
else string.removeSurrounding("[", "]").split(",").filter(String::isNotEmpty).mapNotNull {
val pairs = it.split("|")
if (pairs.size == 2) Pair(pairs[0], pairs[1])
else null
}
}
@JvmName("pairStringListToByteArray")
@TypeConverter
@JvmStatic
fun toByteArray(list: List<Pair<String, String>>): ByteArray =
list.map { it.toList().joinToString("|") }.toString().toByteArray()
@TypeConverter
@JvmStatic
fun toAuthor(byteArray: ByteArray) = Author.fromJson(String(byteArray))
@TypeConverter
@JvmStatic
fun toByteArray(author: Author) = author.toJSON().toByteArray()
@TypeConverter
@JvmStatic
fun toReleases(byteArray: ByteArray): List<Release> =
if (String(byteArray) == "") emptyList()
else String(byteArray).split("|").map { Release.fromJson(it) }
@JvmName("releasesToByteArray")
@TypeConverter
@JvmStatic
fun toByteArray(releases: List<Release>) =
if (releases.isNotEmpty()) releases.joinToString("|") { it.toJSON() }.toByteArray()
else "".toByteArray()
@TypeConverter
@JvmStatic
fun toIncompatibilities(byteArray: ByteArray): List<Release.Incompatibility> =
if (String(byteArray) == "") emptyList()
else String(byteArray).split("|").map { Release.Incompatibility.fromJson(it) }
@JvmName("incompatibilitiesToByteArray")
@TypeConverter
@JvmStatic
fun toByteArray(incompatibilities: List<Release.Incompatibility>) =
if (incompatibilities.isNotEmpty())
incompatibilities.joinToString("|") { it.toJSON() }.toByteArray()
else "".toByteArray()
@TypeConverter
@JvmStatic
fun toScreenshots(byteArray: ByteArray): List<Screenshot> =
if (String(byteArray) == "") emptyList()
else String(byteArray).split("|").map { Screenshot.fromJson(it) }
@JvmName("screenshotsToByteArray")
@TypeConverter
@JvmStatic
fun toByteArray(screenshots: List<Screenshot>) =
if (screenshots.isNotEmpty()) screenshots.joinToString("|") { it.toJSON() }.toByteArray()
else "".toByteArray()
@TypeConverter
@JvmStatic
fun toDonates(byteArray: ByteArray): List<Donate> =
if (String(byteArray) == "") emptyList()
else String(byteArray).split("|").map { Donate.fromJson(it) }
@JvmName("donatesToByteArray")
@TypeConverter
@JvmStatic
fun toByteArray(donates: List<Donate>) =
if (donates.isNotEmpty()) donates.joinToString("|") { it.toJSON() }.toByteArray()
else "".toByteArray()
}

View File

@ -0,0 +1,110 @@
package com.machiav3lli.fdroid.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.machiav3lli.fdroid.database.dao.CategoryDao
import com.machiav3lli.fdroid.database.dao.CategoryTempDao
import com.machiav3lli.fdroid.database.dao.ExtrasDao
import com.machiav3lli.fdroid.database.dao.InstalledDao
import com.machiav3lli.fdroid.database.dao.ProductDao
import com.machiav3lli.fdroid.database.dao.ProductTempDao
import com.machiav3lli.fdroid.database.dao.ReleaseDao
import com.machiav3lli.fdroid.database.dao.RepositoryDao
import com.machiav3lli.fdroid.database.entity.Category
import com.machiav3lli.fdroid.database.entity.CategoryTemp
import com.machiav3lli.fdroid.database.entity.Extras
import com.machiav3lli.fdroid.database.entity.Installed
import com.machiav3lli.fdroid.database.entity.Product
import com.machiav3lli.fdroid.database.entity.ProductTemp
import com.machiav3lli.fdroid.database.entity.Release
import com.machiav3lli.fdroid.database.entity.Repository
import com.machiav3lli.fdroid.database.entity.Repository.Companion.defaultRepositories
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@Database(
entities = [
Repository::class,
Product::class,
Release::class,
ProductTemp::class,
Category::class,
CategoryTemp::class,
Installed::class,
Extras::class
], version = 8
)
@TypeConverters(Converters::class)
abstract class DatabaseX : RoomDatabase() {
abstract val repositoryDao: RepositoryDao
abstract val productDao: ProductDao
abstract val releaseDao: ReleaseDao
abstract val productTempDao: ProductTempDao
abstract val categoryDao: CategoryDao
abstract val categoryTempDao: CategoryTempDao
abstract val installedDao: InstalledDao
abstract val extrasDao: ExtrasDao
companion object {
@Volatile
private var INSTANCE: DatabaseX? = null
fun getInstance(context: Context): DatabaseX {
synchronized(this) {
if (INSTANCE == null) {
INSTANCE = Room
.databaseBuilder(
context.applicationContext,
DatabaseX::class.java,
"main_database.db"
)
.fallbackToDestructiveMigration()
.build()
INSTANCE?.let { instance ->
GlobalScope.launch(Dispatchers.IO) {
if (instance.repositoryDao.count == 0) defaultRepositories.forEach {
instance.repositoryDao.put(it)
}
}
}
}
return INSTANCE!!
}
}
}
fun cleanUp(pairs: Set<Pair<Long, Boolean>>) {
runInTransaction {
pairs.windowed(10, 10, true).map {
it.map { it.first }
.toLongArray()
.forEach { id ->
productDao.deleteById(id)
categoryDao.deleteById(id)
}
it.filter { it.second }
.map { it.first }
.toLongArray()
.forEach { id -> repositoryDao.deleteById(id) }
}
}
}
fun finishTemporary(repository: Repository, success: Boolean) {
runInTransaction {
if (success) {
productDao.deleteById(repository.id)
categoryDao.deleteById(repository.id)
productDao.insert(*(productTempDao.all))
categoryDao.insert(*(categoryTempDao.all))
repositoryDao.put(repository)
}
productTempDao.emptyTable()
categoryTempDao.emptyTable()
}
}
}

View File

@ -0,0 +1,30 @@
package com.machiav3lli.fdroid.database
class QueryBuilder {
companion object {
fun trimQuery(query: String): String {
return query.lines().map { it.trim() }.filter { it.isNotEmpty() }
.joinToString(separator = " ")
}
}
private val builder = StringBuilder()
val arguments = mutableListOf<String>()
operator fun plusAssign(query: String) {
if (builder.isNotEmpty()) {
builder.append(" ")
}
builder.append(trimQuery(query))
}
operator fun remAssign(argument: String) {
this.arguments += argument
}
operator fun remAssign(arguments: List<String>) {
this.arguments += arguments
}
fun build() = builder.toString()
}

View File

@ -0,0 +1,17 @@
package com.machiav3lli.fdroid.database.dao
import androidx.room.*
interface BaseDao<T> {
@Insert
fun insert(vararg product: T)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertReplace(vararg product: T)
@Update(onConflict = OnConflictStrategy.REPLACE)
fun update(vararg obj: T): Int
@Delete
fun delete(obj: T)
}

View File

@ -0,0 +1,40 @@
package com.machiav3lli.fdroid.database.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Query
import com.machiav3lli.fdroid.database.entity.Category
import com.machiav3lli.fdroid.database.entity.CategoryTemp
@Dao
interface CategoryDao : BaseDao<Category> {
@get:Query(
"""SELECT DISTINCT category.label
FROM category AS category
JOIN repository AS repository
ON category.repositoryId = repository._id
WHERE repository.enabled != 0"""
)
val allNames: List<String>
@get:Query(
"""SELECT DISTINCT category.label
FROM category AS category
JOIN repository AS repository
ON category.repositoryId = repository._id
WHERE repository.enabled != 0"""
)
val allNamesLive: LiveData<List<String>>
@Query("DELETE FROM category WHERE repositoryId = :id")
fun deleteById(id: Long): Int
}
@Dao
interface CategoryTempDao : BaseDao<CategoryTemp> {
@get:Query("SELECT * FROM temporary_category")
val all: Array<CategoryTemp>
@Query("DELETE FROM temporary_category")
fun emptyTable()
}

View File

@ -0,0 +1,29 @@
package com.machiav3lli.fdroid.database.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Query
import com.machiav3lli.fdroid.database.entity.Installed
// TODO make sure that apps that not uninstalled by Droid-ify still get removed
@Dao
interface InstalledDao : BaseDao<Installed> {
fun put(vararg installed: Installed) {
installed.forEach { insertReplace(it) }
}
@get:Query("SELECT * FROM memory_installed")
val allLive: LiveData<List<Installed>>
@Query("SELECT * FROM memory_installed WHERE packageName = :packageName")
fun get(packageName: String): Installed?
@Query("SELECT * FROM memory_installed WHERE packageName = :packageName")
fun getLive(packageName: String): LiveData<Installed?>
@Query("DELETE FROM memory_installed WHERE packageName = :packageName")
fun delete(packageName: String)
@Query("DELETE FROM memory_installed")
fun emptyTable()
}

View File

@ -0,0 +1,277 @@
package com.machiav3lli.fdroid.database.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import com.machiav3lli.fdroid.ROW_ADDED
import com.machiav3lli.fdroid.ROW_ANTIFEATURES
import com.machiav3lli.fdroid.ROW_CAN_UPDATE
import com.machiav3lli.fdroid.ROW_CATEGORIES
import com.machiav3lli.fdroid.ROW_COMPATIBLE
import com.machiav3lli.fdroid.ROW_DESCRIPTION
import com.machiav3lli.fdroid.ROW_DONATES
import com.machiav3lli.fdroid.ROW_ENABLED
import com.machiav3lli.fdroid.ROW_ICON
import com.machiav3lli.fdroid.ROW_ID
import com.machiav3lli.fdroid.ROW_IGNORED_VERSION
import com.machiav3lli.fdroid.ROW_IGNORE_UPDATES
import com.machiav3lli.fdroid.ROW_LABEL
import com.machiav3lli.fdroid.ROW_LICENSES
import com.machiav3lli.fdroid.ROW_MATCH_RANK
import com.machiav3lli.fdroid.ROW_METADATA_ICON
import com.machiav3lli.fdroid.ROW_PACKAGE_NAME
import com.machiav3lli.fdroid.ROW_RELEASES
import com.machiav3lli.fdroid.ROW_REPOSITORY_ID
import com.machiav3lli.fdroid.ROW_SCREENSHOTS
import com.machiav3lli.fdroid.ROW_SIGNATURE
import com.machiav3lli.fdroid.ROW_SIGNATURES
import com.machiav3lli.fdroid.ROW_SUMMARY
import com.machiav3lli.fdroid.ROW_UPDATED
import com.machiav3lli.fdroid.ROW_VERSION_CODE
import com.machiav3lli.fdroid.TABLE_CATEGORY
import com.machiav3lli.fdroid.TABLE_CATEGORY_NAME
import com.machiav3lli.fdroid.TABLE_EXTRAS
import com.machiav3lli.fdroid.TABLE_EXTRAS_NAME
import com.machiav3lli.fdroid.TABLE_INSTALLED
import com.machiav3lli.fdroid.TABLE_INSTALLED_NAME
import com.machiav3lli.fdroid.TABLE_PRODUCT
import com.machiav3lli.fdroid.TABLE_PRODUCT_NAME
import com.machiav3lli.fdroid.TABLE_REPOSITORY
import com.machiav3lli.fdroid.TABLE_REPOSITORY_NAME
import com.machiav3lli.fdroid.database.QueryBuilder
import com.machiav3lli.fdroid.database.entity.CategoryTemp
import com.machiav3lli.fdroid.database.entity.Extras
import com.machiav3lli.fdroid.database.entity.Product
import com.machiav3lli.fdroid.database.entity.ProductTemp
import com.machiav3lli.fdroid.database.entity.asProductTemp
import com.machiav3lli.fdroid.entity.Order
import com.machiav3lli.fdroid.entity.Section
import com.machiav3lli.fdroid.entity.UpdateCategory
import com.machiav3lli.fdroid.ui.fragments.Request
@Dao
interface ProductDao : BaseDao<Product> {
@Query("SELECT COUNT(*) FROM product WHERE repositoryId = :id")
fun countForRepository(id: Long): Long
@Query("SELECT COUNT(*) FROM product WHERE repositoryId = :id")
fun countForRepositoryLive(id: Long): LiveData<Long>
@Query("SELECT * FROM product WHERE packageName = :packageName")
fun get(packageName: String): List<Product?>
@Query("SELECT * FROM product WHERE packageName = :packageName")
fun getLive(packageName: String): LiveData<List<Product?>>
@Query("DELETE FROM product WHERE repositoryId = :id")
fun deleteById(id: Long): Int
@RawQuery
fun queryObject(query: SupportSQLiteQuery): List<Product>
fun queryObject(request: Request): List<Product> = queryObject(
buildProductQuery(
request.installed,
request.updates,
request.searchQuery,
request.section,
request.order,
request.numberOfItems,
request.updateCategory
)
)
@Transaction
fun queryObject(
installed: Boolean, updates: Boolean, searchQuery: String,
section: Section, order: Order, numberOfItems: Int = 0,
updateCategory: UpdateCategory = UpdateCategory.ALL
): List<Product> = queryObject(
buildProductQuery(
installed,
updates,
searchQuery,
section,
order,
numberOfItems,
updateCategory
)
)
@RawQuery(observedEntities = [Product::class])
fun queryLiveList(query: SupportSQLiteQuery): LiveData<List<Product>>
fun queryLiveList(request: Request): LiveData<List<Product>> = queryLiveList(
buildProductQuery(
request.installed,
request.updates,
request.searchQuery,
request.section,
request.order,
request.numberOfItems,
request.updateCategory
)
)
fun buildProductQuery(
installed: Boolean,
updates: Boolean,
searchQuery: String,
section: Section,
order: Order,
numberOfItems: Int = 0,
updateCategory: UpdateCategory = UpdateCategory.ALL
): SupportSQLiteQuery {
val builder = QueryBuilder()
// TODO improve signature matching logic
val signatureMatches = """$TABLE_INSTALLED.$ROW_SIGNATURE IS NOT NULL AND
$TABLE_PRODUCT.$ROW_SIGNATURES LIKE ('%' || $TABLE_INSTALLED.$ROW_SIGNATURE || '%') AND
$TABLE_PRODUCT.$ROW_SIGNATURES != ''"""
// Select the return fields
builder += """SELECT $TABLE_PRODUCT.rowid AS $ROW_ID, $TABLE_PRODUCT.$ROW_REPOSITORY_ID,
$TABLE_PRODUCT.$ROW_PACKAGE_NAME, $TABLE_PRODUCT.$ROW_LABEL,
$TABLE_PRODUCT.$ROW_SUMMARY, $TABLE_PRODUCT.$ROW_DESCRIPTION,
(COALESCE($TABLE_EXTRAS.$ROW_IGNORED_VERSION, -1) != $TABLE_PRODUCT.$ROW_VERSION_CODE AND
COALESCE($TABLE_EXTRAS.$ROW_IGNORE_UPDATES, 0) = 0 AND $TABLE_PRODUCT.$ROW_COMPATIBLE != 0 AND
$TABLE_PRODUCT.$ROW_VERSION_CODE > COALESCE($TABLE_INSTALLED.$ROW_VERSION_CODE, 0xffffffff) AND
$signatureMatches) AS $ROW_CAN_UPDATE, $TABLE_PRODUCT.$ROW_ICON,
$TABLE_PRODUCT.$ROW_METADATA_ICON, $TABLE_PRODUCT.$ROW_RELEASES, $TABLE_PRODUCT.$ROW_CATEGORIES,
$TABLE_PRODUCT.$ROW_ANTIFEATURES, $TABLE_PRODUCT.$ROW_LICENSES, $TABLE_PRODUCT.$ROW_DONATES,
$TABLE_PRODUCT.$ROW_SCREENSHOTS, $TABLE_PRODUCT.$ROW_VERSION_CODE,"""
// Calculate the matching score with the search query
if (searchQuery.isNotEmpty()) {
builder += """((($TABLE_PRODUCT.$ROW_LABEL LIKE ? OR
$TABLE_PRODUCT.$ROW_SUMMARY LIKE ?) * 7) |
(($TABLE_PRODUCT.$ROW_PACKAGE_NAME LIKE ?) * 3) |
($TABLE_PRODUCT.$ROW_DESCRIPTION LIKE ?)) AS $ROW_MATCH_RANK,"""
builder %= List(4) { "%$searchQuery%" }
} else {
builder += "0 AS $ROW_MATCH_RANK,"
}
// Take product as main table
builder += """MAX(($TABLE_PRODUCT.$ROW_COMPATIBLE AND
($TABLE_INSTALLED.$ROW_SIGNATURE IS NULL OR $signatureMatches)) ||
PRINTF('%016X', $TABLE_PRODUCT.$ROW_VERSION_CODE)) FROM $TABLE_PRODUCT_NAME AS $TABLE_PRODUCT"""
// Merge the matching repositories
builder += """JOIN $TABLE_REPOSITORY_NAME AS $TABLE_REPOSITORY
ON $TABLE_PRODUCT.$ROW_REPOSITORY_ID = $TABLE_REPOSITORY.$ROW_ID"""
// Merge the matching extras
builder += """LEFT JOIN $TABLE_EXTRAS_NAME AS $TABLE_EXTRAS
ON $TABLE_PRODUCT.$ROW_PACKAGE_NAME = $TABLE_EXTRAS.$ROW_PACKAGE_NAME"""
// Merge the matching installed
if (!installed && !updates) builder += "LEFT"
builder += """JOIN $TABLE_INSTALLED_NAME AS $TABLE_INSTALLED
ON $TABLE_PRODUCT.$ROW_PACKAGE_NAME = $TABLE_INSTALLED.$ROW_PACKAGE_NAME"""
// Merge the matching category
if (section is Section.Category) {
builder += """JOIN $TABLE_CATEGORY_NAME AS $TABLE_CATEGORY
ON $TABLE_PRODUCT.$ROW_PACKAGE_NAME = $TABLE_CATEGORY.$ROW_PACKAGE_NAME"""
}
// Filter only active repositories
builder += """WHERE $TABLE_REPOSITORY.$ROW_ENABLED != 0"""
// Filter only the selected repository/category
if (section is Section.Category) {
builder += "AND $TABLE_CATEGORY.$ROW_LABEL = ?"
builder %= section.name
} else if (section is Section.Repository) {
builder += "AND $TABLE_PRODUCT.$ROW_REPOSITORY_ID = ?"
builder %= section.id.toString()
}
// Filter only apps that have some matching score to the search query
if (searchQuery.isNotEmpty()) {
builder += """AND $ROW_MATCH_RANK > 0"""
}
when (updateCategory) {
UpdateCategory.ALL -> Unit
UpdateCategory.NEW -> builder += """AND $TABLE_PRODUCT.$ROW_ADDED = $TABLE_PRODUCT.$ROW_UPDATED"""
UpdateCategory.UPDATED -> builder += """AND $TABLE_PRODUCT.$ROW_ADDED < $TABLE_PRODUCT.$ROW_UPDATED"""
}
// Sum up all products with the same package name
builder += "GROUP BY $TABLE_PRODUCT.$ROW_PACKAGE_NAME HAVING 1"
// Filter if only can update
if (updates) {
builder += "AND $ROW_CAN_UPDATE"
}
// Set sorting order
builder += "ORDER BY"
if (searchQuery.isNotEmpty()) builder += """$ROW_MATCH_RANK DESC,"""
when (order) {
Order.NAME -> Unit
Order.DATE_ADDED -> builder += "$TABLE_PRODUCT.$ROW_ADDED DESC,"
Order.LAST_UPDATE -> builder += "$TABLE_PRODUCT.$ROW_UPDATED DESC,"
}::class
builder += "$TABLE_PRODUCT.$ROW_LABEL COLLATE LOCALIZED ASC${if (numberOfItems > 0) " LIMIT $numberOfItems" else ""}"
return SimpleSQLiteQuery(builder.build(), builder.arguments.toTypedArray())
}
}
@Dao
interface ProductTempDao : BaseDao<ProductTemp> {
@get:Query("SELECT * FROM temporary_product")
val all: Array<ProductTemp>
@Query("DELETE FROM temporary_product")
fun emptyTable()
@Insert
fun insertCategory(vararg product: CategoryTemp)
@Transaction
fun putTemporary(products: List<Product>) {
products.forEach {
insert(it.asProductTemp())
it.categories.forEach { category ->
insertCategory(CategoryTemp().apply {
repositoryId = it.repositoryId
packageName = it.packageName
label = category
})
}
}
}
}
@Dao
interface ExtrasDao : BaseDao<Extras> {
@Query("DELETE FROM extras WHERE packageName = :packageName")
fun delete(packageName: String)
@Query("SELECT * FROM extras WHERE packageName = :packageName")
operator fun get(packageName: String): Extras?
@Query("SELECT * FROM extras WHERE packageName = :packageName")
fun getLive(packageName: String): LiveData<Extras?>
@get:Query("SELECT * FROM extras")
val all: List<Extras>
@get:Query("SELECT * FROM extras")
val allLive: LiveData<List<Extras>>
@get:Query("SELECT packageName FROM extras WHERE favorite != 0")
val favorites: Array<String>
@get:Query("SELECT packageName FROM extras WHERE favorite != 0")
val favoritesLive: LiveData<Array<String>>
}

View File

@ -0,0 +1,16 @@
package com.machiav3lli.fdroid.database.dao
import androidx.room.Dao
import androidx.room.Query
import com.machiav3lli.fdroid.database.entity.Release
@Dao
interface ReleaseDao : BaseDao<Release> {
// This one for the mode combining releases of different sources
@Query("SELECT * FROM `release` WHERE packageName = :packageName")
fun get(packageName: String): List<Release?>
// This one for the separating releases of different sources
@Query("SELECT * FROM `release` WHERE packageName = :packageName AND signature = :signature")
fun get(packageName: String, signature: String): List<Release?>
}

View File

@ -0,0 +1,49 @@
package com.machiav3lli.fdroid.database.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.machiav3lli.fdroid.database.entity.Repository
import kotlinx.coroutines.flow.Flow
@Dao
interface RepositoryDao : BaseDao<Repository> {
@get:Query("SELECT COUNT(_id) FROM repository")
val count: Int
fun put(repository: Repository): Repository {
repository.let { item ->
val newId = if (item.id > 0L) update(item).toLong() else returnInsert(item)
return if (newId != repository.id) repository.copy(id = newId) else repository
}
}
@Insert
fun returnInsert(product: Repository): Long
@Query("SELECT * FROM repository WHERE _id = :id")
fun get(id: Long): Repository?
@Query("SELECT * FROM repository WHERE _id = :id")
fun getLive(id: Long): LiveData<Repository?>
@Query("SELECT * FROM repository ORDER BY _id ASC")
fun getAllRepositories(): Flow<List<Repository>>
@get:Query("SELECT * FROM repository ORDER BY _id ASC")
val all: List<Repository>
@get:Query("SELECT * FROM repository ORDER BY _id ASC")
val allLive: LiveData<List<Repository>>
@get:Query("SELECT _id FROM repository WHERE enabled == 0 ORDER BY _id ASC")
val allDisabled: List<Long>
// TODO clean up products and other tables afterwards
@Query("DELETE FROM repository WHERE _id = :id")
fun deleteById(id: Long)
@Query("SELECT MAX(_id) FROM repository")
fun latestAddedId(): Long
}

View File

@ -0,0 +1,17 @@
package com.machiav3lli.fdroid.database.entity
import androidx.room.Entity
import com.machiav3lli.fdroid.*
@Entity(
tableName = TABLE_CATEGORY_NAME,
primaryKeys = [ROW_REPOSITORY_ID, ROW_PACKAGE_NAME, ROW_LABEL]
)
open class Category {
var repositoryId: Long = 0
var packageName = ""
var label = ""
}
@Entity(tableName = TABLE_CATEGORY_TEMP_NAME)
class CategoryTemp : Category()

View File

@ -0,0 +1,14 @@
package com.machiav3lli.fdroid.database.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.machiav3lli.fdroid.TABLE_EXTRAS_NAME
@Entity(tableName = TABLE_EXTRAS_NAME)
data class Extras(
@PrimaryKey
var packageName: String = "",
var favorite: Boolean = false,
var ignoreUpdates: Boolean = false,
var ignoredVersion: Long = 0L,
)

View File

@ -0,0 +1,16 @@
package com.machiav3lli.fdroid.database.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.machiav3lli.fdroid.TABLE_INSTALLED_NAME
@Entity(tableName = TABLE_INSTALLED_NAME)
data class Installed(
@PrimaryKey
var packageName: String = "",
var version: String = "",
var versionCode: Long = 0L,
var signature: String = "",
var isSystem: Boolean = false,
val launcherActivities: List<Pair<String, String>> = emptyList()
)

View File

@ -0,0 +1,210 @@
package com.machiav3lli.fdroid.database.entity
import androidx.room.Entity
import com.machiav3lli.fdroid.ROW_PACKAGE_NAME
import com.machiav3lli.fdroid.ROW_REPOSITORY_ID
import com.machiav3lli.fdroid.TABLE_PRODUCT_NAME
import com.machiav3lli.fdroid.TABLE_PRODUCT_TEMP_NAME
import com.machiav3lli.fdroid.entity.Author
import com.machiav3lli.fdroid.entity.Donate
import com.machiav3lli.fdroid.entity.ProductItem
import com.machiav3lli.fdroid.entity.Screenshot
import com.machiav3lli.fdroid.utility.extension.text.nullIfEmpty
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
// TODO Add Product Extras to handle favorite lists etc..
@Entity(tableName = TABLE_PRODUCT_NAME, primaryKeys = [ROW_REPOSITORY_ID, ROW_PACKAGE_NAME])
@Serializable
open class Product(
var repositoryId: Long,
var packageName: String
) {
var label: String = ""
var summary: String = ""
var description: String = ""
var added: Long = 0L
var updated: Long = 0L
var icon: String = ""
var metadataIcon: String = ""
var releases: List<Release> = emptyList()
var categories: List<String> = emptyList()
var antiFeatures: List<String> = emptyList()
var licenses: List<String> = emptyList()
var donates: List<Donate> = emptyList()
var screenshots: List<Screenshot> = emptyList()
var versionCode: Long = 0L
var suggestedVersionCode: Long = 0L
var signatures: List<String> = emptyList()
var compatible: Boolean = false
var author: Author = Author()
var source: String = ""
var web: String = ""
var tracker: String = ""
var changelog: String = ""
var whatsNew: String = ""
constructor(
repositoryId: Long,
packageName: String,
label: String,
summary: String,
description: String,
added: Long,
updated: Long,
icon: String,
metadataIcon: String,
releases: List<Release>,
categories: List<String>,
antiFeatures: List<String>,
licenses: List<String>,
donates: List<Donate>,
screenshots: List<Screenshot>,
suggestedVersionCode: Long = 0L,
author: Author = Author(),
source: String = "",
web: String = "",
tracker: String = "",
changelog: String = "",
whatsNew: String = ""
) : this(repositoryId, packageName) {
this.label = label
this.summary = summary
this.description = description
this.added = added
this.updated = updated
this.icon = icon
this.metadataIcon = metadataIcon
this.releases = releases
this.categories = categories
this.antiFeatures = antiFeatures
this.licenses = licenses
this.donates = donates
this.screenshots = screenshots
this.versionCode = selectedReleases.firstOrNull()?.versionCode ?: 0L
this.suggestedVersionCode = suggestedVersionCode
this.signatures = selectedReleases.mapNotNull { it.signature.nullIfEmpty() }.distinct()
this.compatible = selectedReleases.firstOrNull()?.incompatibilities?.isEmpty() == true
this.author = author
this.source = source
this.web = web
this.tracker = tracker
this.changelog = changelog
this.whatsNew = whatsNew
}
val selectedReleases: List<Release>
get() = releases.filter { it.selected }
val displayRelease: Release?
get() = selectedReleases.firstOrNull() ?: releases.firstOrNull()
val version: String
get() = displayRelease?.version.orEmpty()
fun toItem(installed: Installed? = null): ProductItem =
ProductItem(
repositoryId,
packageName,
label,
summary,
icon,
metadataIcon,
version,
"",
compatible,
canUpdate(installed),
0
)
fun canUpdate(installed: Installed?): Boolean =
installed != null && compatible && versionCode > installed.versionCode && installed.signature in signatures
fun refreshVariables() {
this.versionCode = selectedReleases.firstOrNull()?.versionCode ?: 0L
this.signatures = selectedReleases.mapNotNull { it.signature.nullIfEmpty() }.distinct()
this.compatible = selectedReleases.firstOrNull()?.incompatibilities?.isEmpty() == true
}
fun toJSON() = Json.encodeToString(this)
companion object {
fun fromJson(json: String) = Json.decodeFromString<Product>(json)
}
}
@Entity(tableName = TABLE_PRODUCT_TEMP_NAME)
class ProductTemp(
repositoryId: Long,
packageName: String,
label: String,
summary: String,
description: String,
added: Long,
updated: Long,
icon: String,
metadataIcon: String,
releases: List<Release>,
categories: List<String>,
antiFeatures: List<String>,
licenses: List<String>,
donates: List<Donate>,
screenshots: List<Screenshot>,
suggestedVersionCode: Long = 0L,
author: Author = Author(),
source: String = "",
web: String = "",
tracker: String = "",
changelog: String = "",
whatsNew: String = ""
) : Product(
repositoryId = repositoryId,
packageName = packageName,
label = label,
summary = summary,
description = description,
added = added,
updated = updated,
icon = icon,
metadataIcon = metadataIcon,
releases = releases,
categories = categories,
antiFeatures = antiFeatures,
licenses = licenses,
donates = donates,
screenshots = screenshots,
suggestedVersionCode = suggestedVersionCode,
author = author,
source = source,
web = web,
tracker = tracker,
changelog = changelog,
whatsNew = whatsNew
)
fun Product.asProductTemp(): ProductTemp = ProductTemp(
repositoryId = repositoryId,
packageName = packageName,
label = label,
summary = summary,
description = description,
added = added,
updated = updated,
icon = icon,
metadataIcon = metadataIcon,
releases = releases,
categories = categories,
antiFeatures = antiFeatures,
licenses = licenses,
donates = donates,
screenshots = screenshots,
suggestedVersionCode = suggestedVersionCode,
author = author,
source = source,
web = web,
tracker = tracker,
changelog = changelog,
whatsNew = whatsNew
)

View File

@ -0,0 +1,82 @@
package com.machiav3lli.fdroid.database.entity
import android.net.Uri
import androidx.room.Entity
import com.machiav3lli.fdroid.ROW_PACKAGE_NAME
import com.machiav3lli.fdroid.ROW_SIGNATURE
import com.machiav3lli.fdroid.ROW_VERSION_CODE
import com.machiav3lli.fdroid.TABLE_RELEASE_NAME
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
// TODO make a Room entity
@Entity(
tableName = TABLE_RELEASE_NAME,
primaryKeys = [ROW_PACKAGE_NAME, ROW_VERSION_CODE, ROW_SIGNATURE]
)
@Serializable
data class Release(
val packageName: String,
val selected: Boolean,
val version: String,
val versionCode: Long,
val added: Long,
val size: Long,
val minSdkVersion: Int,
val targetSdkVersion: Int,
val maxSdkVersion: Int,
val source: String,
val release: String,
val hash: String,
val hashType: String,
val signature: String,
val obbMain: String,
val obbMainHash: String,
val obbMainHashType: String,
val obbPatch: String,
val obbPatchHash: String,
val obbPatchHashType: String,
val permissions: List<String>,
val features: List<String>,
val platforms: List<String>,
val incompatibilities: List<Incompatibility>,
) {
@Serializable
sealed class Incompatibility {
@Serializable
object MinSdk : Incompatibility()
@Serializable
object MaxSdk : Incompatibility()
@Serializable
object Platform : Incompatibility()
@Serializable
data class Feature(val feature: String) : Incompatibility()
fun toJSON() = Json.encodeToString(this)
companion object {
fun fromJson(json: String) = Json.decodeFromString<Incompatibility>(json)
}
}
val identifier: String
get() = "$versionCode.$hash"
fun getDownloadUrl(repository: Repository): String {
return Uri.parse(repository.address).buildUpon().appendPath(release).build().toString()
}
val cacheFileName: String
get() = "${hash.replace('/', '-')}.apk"
fun toJSON() = Json.encodeToString(this)
companion object {
fun fromJson(json: String) = Json.decodeFromString<Release>(json)
}
}

View File

@ -0,0 +1,289 @@
package com.machiav3lli.fdroid.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.machiav3lli.fdroid.ROW_ID
import com.machiav3lli.fdroid.TABLE_REPOSITORY_NAME
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.net.URL
@Entity(tableName = TABLE_REPOSITORY_NAME)
@Serializable
data class Repository(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = ROW_ID)
var id: Long = 0,
var address: String = "",
var mirrors: List<String> = emptyList(),
var name: String = "",
var description: String = "",
var version: Int = 21,
var enabled: Boolean = false,
var fingerprint: String = "",
var lastModified: String = "",
var entityTag: String = "",
var updated: Long = 0L,
var timestamp: Long = 0L,
var authentication: String = "",
) {
fun edit(address: String, fingerprint: String, authentication: String): Repository = apply {
val changed = this.address != address || this.fingerprint != fingerprint
this.lastModified = if (changed) "" else lastModified
this.entityTag = if (changed) "" else entityTag
this.address = address
this.fingerprint = fingerprint
this.authentication = authentication
}
fun update(
mirrors: List<String>, name: String, description: String, version: Int,
lastModified: String, entityTag: String, timestamp: Long,
): Repository = apply {
this.mirrors = mirrors
this.name = name
this.description = description
this.version = if (version >= 0) version else this.version
this.lastModified = lastModified
this.entityTag = entityTag
this.updated = System.currentTimeMillis()
this.timestamp = timestamp
}
fun enable(enabled: Boolean): Repository = apply {
this.enabled = enabled
this.lastModified = ""
this.entityTag = ""
}
fun toJSON() = Json.encodeToString(this)
companion object {
fun fromJson(json: String) = Json.decodeFromString<Repository>(json)
fun newRepository(
address: String = "",
fingerprint: String = "",
authentication: String = "",
): Repository {
val name = try {
URL(address).let { "${it.host}${it.path}" }
} catch (e: Exception) {
address
}
return Repository(
address = address,
name = name,
fingerprint = fingerprint,
authentication = authentication
)
}
private fun defaultRepository(
address: String, name: String, description: String,
version: Int, enabled: Boolean, fingerprint: String, authentication: String,
): Repository = Repository(
0, address, emptyList(), name, description, version, enabled,
fingerprint, "", "", 0L, 0L, authentication
)
val defaultRepositories = listOf(run {
defaultRepository(
"https://f-droid.org/repo",
"F-Droid",
"The official F-Droid Free Software repository. " +
"Everything in this repository is always built from the source code.",
21,
true,
"43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB",
""
)
}, run {
defaultRepository(
"https://f-droid.org/archive",
"F-Droid Archive",
"The archive of the official F-Droid Free " +
"Software repository. Apps here are old and can contain known vulnerabilities and security issues!",
21,
false,
"43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB",
""
)
}, run {
defaultRepository(
"https://guardianproject.info/fdroid/repo",
"Guardian Project Official Releases",
"The " +
"official repository of The Guardian Project apps for use with the F-Droid client. Applications in this " +
"repository are official binaries built by the original application developers and signed by the same key as " +
"the APKs that are released in the Google Play Store.",
21,
false,
"B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135",
""
)
}, run {
defaultRepository(
"https://guardianproject.info/fdroid/archive",
"Guardian Project Archive",
"The official " +
"repository of The Guardian Project apps for use with the F-Droid client. This contains older versions of " +
"applications from the main repository.",
21,
false,
"B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135",
""
)
}, run {
defaultRepository(
"https://apt.izzysoft.de/fdroid/repo", "IzzyOnDroid F-Droid Repo", "This is a " +
"repository of apps to be used with F-Droid the original application developers, taken from the resp. " +
"repositories (mostly GitHub). At this moment I cannot give guarantees on regular updates for all of them, " +
"though most are checked multiple times a week ", 21, true,
"3BF0D6ABFEAE2F401707B6D966BE743BF0EEE49C2561B9BA39073711F628937A", ""
)
}, run {
defaultRepository(
"https://microg.org/fdroid/repo", "MicroG Project",
"Official repository of the open-source implementation of Google Play Services.",
21, false, "9BD06727E62796C0130EB6DAB39B73157451582CBD138E86C468ACC395D14165", ""
)
}, run {
defaultRepository(
"https://repo.netsyms.com/fdroid/repo", "Netsyms Technologies",
"Open-source apps created by Netsyms Technologies.",
21, false, "2581BA7B32D3AB443180C4087CAB6A7E8FB258D3A6E98870ECB3C675E4D64489", ""
)
}, run {
defaultRepository(
"https://fdroid.bromite.org/fdroid/repo", "Bromite",
"Bromite is a Chromium plus ad blocking and enhanced privacy; take back your browser.",
21, false, "E1EE5CD076D7B0DC84CB2B45FB78B86DF2EB39A3B6C56BA3DC292A5E0C3B9504", ""
)
}, run {
defaultRepository(
"https://molly.im/fdroid/foss/fdroid/repo", "Molly",
"Molly is a fork of Signal focused on security.",
21, false, "5198DAEF37FC23C14D5EE32305B2AF45787BD7DF2034DE33AD302BDB3446DF74", ""
)
}, run {
defaultRepository(
"https://archive.newpipe.net/fdroid/repo", "NewPipe",
"NewPipe's official independent repository.",
21, false, "E2402C78F9B97C6C89E97DB914A2751FDA1D02FE2039CC0897A462BDB57E7501", ""
)
}, run {
defaultRepository(
"https://www.collaboraoffice.com/downloads/fdroid/repo", "Collabora Office",
"Collabora Office is an office suite based on LibreOffice.",
21, false, "573258C84E149B5F4D9299E7434B2B69A8410372921D4AE586BA91EC767892CC", ""
)
}, run {
defaultRepository(
"https://www.droidware.info/fdroid/repo", "Ungoogled Chromium",
"Chromium sans dependency on Google web services. It also features some enhancments to privacy, control & transparency",
21, false, "2144449AB1DD270EC31B6087409B5D0EA39A75A9F290DA62AC1B238A0EAAF851", ""
)
}, run {
defaultRepository(
"https://fdroid.libretro.com/repo", "LibRetro",
"The official canary repository for this great retro emulators hub.",
21, false, "3F05B24D497515F31FEAB421297C79B19552C5C81186B3750B7C131EF41D733D", ""
)
}, run {
defaultRepository(
"https://cdn.kde.org/android/fdroid/repo", "KDE Android",
"The official nightly repository for KDE Android apps.",
21, false, "B3EBE10AFA6C5C400379B34473E843D686C61AE6AD33F423C98AF903F056523F", ""
)
}, run {
defaultRepository(
"https://rfc2822.gitlab.io/fdroid-firefox/fdroid/repo", "Unofficial Firefox",
"An unofficial repository with some of the most well known FOSS apps not on F-Droid.",
21, false, "8F992BBBA0340EFE6299C7A410B36D9C8889114CA6C58013C3587CDA411B4AED", ""
)
}, run {
defaultRepository(
"https://calyxos.gitlab.io/calyx-fdroid-repo/fdroid/repo", "Calyx OS Repo",
"The official Calyx Labs F-Droid repository.",
21, false, "C44D58B4547DE5096138CB0B34A1CC99DAB3B4274412ED753FCCBFC11DC1B7B6", ""
)
}, run {
defaultRepository(
"https://divestos.org/fdroid/official", "Divest OS Repo",
"The official Divest OS F-Droid repository.",
21, false, "E4BE8D6ABFA4D9D4FEEF03CDDA7FF62A73FD64B75566F6DD4E5E577550BE8467", ""
)
}, run {
defaultRepository(
"https://fdroid.fedilab.app/repo", "Fedilab",
"Fedilab's official F-Droid repository.",
21, false, "11F0A69910A4280E2CD3CCC3146337D006BE539B18E1A9FEACE15FF757A94FEB", ""
)
}, run {
defaultRepository(
"https://store.nethunter.com/repo", "Kali Nethunter",
"Kali Nethunter's official selection of original binaries.",
21, false, "7E418D34C3AD4F3C37D7E6B0FACE13332364459C862134EB099A3BDA2CCF4494", ""
)
}, run {
defaultRepository(
"https://secfirst.org/fdroid/repo", "Umbrella",
"Security advices, tutorials, tools etc..",
21, false, "39EB57052F8D684514176819D1645F6A0A7BD943DBC31AB101949006AC0BC228", ""
)
}, run {
defaultRepository(
"https://thecapslock.gitlab.io/fdroid-patched-apps/fdroid/repo", "Patched Apps",
"A collection of patched applications to provide better compatibility, privacy etc..",
21, false, "313D9E6E789FF4E8E2D687AAE31EEF576050003ED67963301821AC6D3763E3AC", ""
)
}, run {
defaultRepository(
"https://mobileapp.bitwarden.com/fdroid/repo", "Bitwarden",
"The official repository for Bitwarden.",
21, false, "BC54EA6FD1CD5175BCCCC47C561C5726E1C3ED7E686B6DB4B18BAC843A3EFE6C", ""
)
}, run {
defaultRepository(
"https://briarproject.org/fdroid/repo", "Briar",
"An serverless/offline messenger that hides your metadata.",
21, false, "1FB874BEE7276D28ECB2C9B06E8A122EC4BCB4008161436CE474C257CBF49BD6", ""
)
}, run {
defaultRepository(
"https://guardianproject-wind.s3.amazonaws.com/fdroid/repo", "Wind Project",
"A collection of interesting offline/serverless apps.",
21, false, "182CF464D219D340DA443C62155198E399FEC1BC4379309B775DD9FC97ED97E1", ""
)
}, run {
defaultRepository(
"https://repo.alefvanoon.xyz/fdroid/repo", "Alefvanoon",
"A collection of open-source apps that for one reason or another not on F-Droid.",
21, false, "04DF198F553069C7BE60F057AE12000E99F7700DA895CC1CE2EB11DC871581F1", ""
)
}, run {
defaultRepository(
"https://repo.unifiedpush.org/", "UnifiedPush",
"A repository of apps for unifiedPush.",
21, false, "DF11176406C63A7876103CE6CC1A8FCA239DF76AA5F6DE5CDBAFE73ECDBF1875", ""
)
}, run {
defaultRepository(
"https://nanolx.org/fdroid/repo", "NanoDroid",
"A companion repository to microG's installer.",
21, false, "862ED9F13A3981432BF86FE93D14596B381D75BE83A1D616E2D44A12654AD015", ""
)
})
}
class IdAndDeleted {
@ColumnInfo(name = "_id")
var id = 0L
var deleted = false
}
}