diff --git a/README.MD b/README.MD index dbd24cf..f4dc776 100644 --- a/README.MD +++ b/README.MD @@ -364,3 +364,102 @@ dependencies { implementation("io.github.kdroidfilter.database:localization:") } ``` + +### Downloader Module + +The downloader module provides functionality to download the latest store and policies databases from GitHub releases. It includes methods to download databases for multiple languages. + +```kotlin +dependencies { + implementation("io.github.kdroidfilter.database.downloader:core:") +} +``` + +#### Usage Example + +```kotlin +// Create a DatabaseDownloader instance +val databaseDownloader = DatabaseDownloader() + +// Download store databases for all languages (en, fr, he) +val outputDir = "path/to/output/directory" +val storeResults = databaseDownloader.downloadLatestStoreDatabases(outputDir) + +// Check download results +storeResults.forEach { (language, success) -> + if (success) { + println("Successfully downloaded store database for $language") + } else { + println("Failed to download store database for $language") + } +} + +// Download policies database +val policiesResult = databaseDownloader.downloadLatestPoliciesDatabase(outputDir) +if (policiesResult) { + println("Successfully downloaded policies database") +} else { + println("Failed to download policies database") +} +``` + +### DAO Module + +The DAO (Data Access Object) module provides a clean interface for database operations, abstracting away direct SQL queries. It includes DAOs for accessing application data and version information, as well as utility classes for working with the data. + +```kotlin +dependencies { + implementation("io.github.kdroidfilter.database.dao:core:") +} +``` + +#### Key Components + +- **ApplicationsDao**: Provides methods for loading and searching applications in the database +- **VersionDao**: Handles database version management operations +- **AppInfoWithExtras**: A data class that extends the GooglePlayApplicationInfo model with additional information + +#### Usage Example + +```kotlin +// Load applications from the database +val applications = ApplicationsDao.loadApplicationsFromDatabase( + database = database, + deviceLanguage = "en", + creator = { id, categoryLocalizedName, appInfo -> + AppInfoWithExtras( + id = id, + categoryLocalizedName = categoryLocalizedName, + app = appInfo + ) + } +) + +// Search for applications in the database +val searchResults = ApplicationsDao.searchApplicationsInDatabase( + database = database, + query = "calculator", + deviceLanguage = "en", + creator = { id, categoryLocalizedName, appInfo -> + AppInfoWithExtras( + id = id, + categoryLocalizedName = categoryLocalizedName, + app = appInfo + ) + } +) + +// Get the current database version +val currentVersion = VersionDao.getCurrentVersion(database) + +// Update the database version +val updateSuccess = VersionDao.updateVersion(database, "NEWVERSION") +``` + +#### Note on Sample Code + +For a simplified overview of how to use the database, please consult the example in the `sample` directory. The sample demonstrates basic database operations including downloading, querying, and displaying data. + +> ⚠️ **Important Warning**: The sample code uses `runBlocking` for database downloads, which is **prohibited** in production code. This is only done for demonstration purposes. In real applications, always use proper coroutine scopes and avoid blocking the main thread. + +The DAO module is actively evolving to satisfy more needs and use cases. Contributions and pull requests are welcome to enhance its functionality and performance. diff --git a/dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/AppModels.kt b/dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/AppInfoWithExtras.kt similarity index 100% rename from dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/AppModels.kt rename to dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/AppInfoWithExtras.kt diff --git a/dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/ApplicationsDao.kt b/dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/ApplicationsDao.kt index dd1bb06..55dc915 100644 --- a/dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/ApplicationsDao.kt +++ b/dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/ApplicationsDao.kt @@ -33,7 +33,9 @@ object ApplicationsDao { score = app.score ?: 0.0, ratings = app.ratings ?: 0L, reviews = app.reviews ?: 0L, - histogram = app.histogram?.removeSurrounding("[", "]")?.split(", ")?.map { it.toLongOrNull() ?: 0L } ?: emptyList(), + histogram = app.histogram?.removeSurrounding("[", "]")?.split(", ")?.map { + it.toLongOrNull() ?: 0L + } ?: emptyList(), price = app.price ?: 0.0, free = app.free == 1L, currency = app.currency ?: "", diff --git a/dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/VersionDao.kt b/dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/VersionDao.kt new file mode 100644 index 0000000..4706461 --- /dev/null +++ b/dao/src/commonMain/kotlin/io/github/kdroidfilter/database/dao/VersionDao.kt @@ -0,0 +1,52 @@ +package io.github.kdroidfilter.database.dao + +import co.touchlab.kermit.Logger +import io.github.kdroidfilter.database.store.Database + +/** + * Data Access Object for Version + * Contains functions for database operations related to version information + */ +object VersionDao { + private val logger = Logger.withTag("VersionDao") + + /** + * Gets the current version from the database + * @param database The database instance + * @return The version string or null if no version is found + */ + fun getCurrentVersion(database: Database): String? { + try { + val versionQueries = database.versionQueries + val version = versionQueries.getVersion().executeAsOneOrNull() + return version?.release_name + } catch (e: Exception) { + logger.e(e) { "Failed to get current version: ${e.message}" } + return null + } + } + + /** + * Updates the version in the database + * @param database The database instance + * @param versionName The new version name + * @return Boolean indicating whether the update was successful + */ + fun updateVersion(database: Database, versionName: String): Boolean { + return try { + val versionQueries = database.versionQueries + + // Clear existing versions + versionQueries.clearVersions() + + // Insert new version + versionQueries.insertVersion(versionName) + + logger.i { "Version updated to $versionName" } + true + } catch (e: Exception) { + logger.e(e) { "Failed to update version: ${e.message}" } + false + } + } +} \ No newline at end of file diff --git a/downloader/build.gradle.kts b/downloader/build.gradle.kts new file mode 100644 index 0000000..7550a50 --- /dev/null +++ b/downloader/build.gradle.kts @@ -0,0 +1,108 @@ +import com.vanniktech.maven.publish.SonatypeHost + +plugins { + alias(libs.plugins.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.vannitktech.maven.publish) +} + +val ref = System.getenv("GITHUB_REF") ?: "" +version = if (ref.startsWith("refs/tags/")) { + val tag = ref.removePrefix("refs/tags/") + if (tag.startsWith("v")) tag.substring(1) else tag +} else "dev" + + +kotlin { + jvmToolchain(17) + androidTarget { publishLibraryVariants("release") } + jvm() + + + sourceSets { + + androidMain.dependencies { + implementation(libs.kotlinx.coroutines.android) + } + + jvmMain.dependencies { + implementation(libs.maven.slf4j.provider) + implementation(libs.kotlinx.coroutines.swing) + } + + jvmTest.dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) + + } + + commonMain.dependencies { + api(project(":core")) + api(libs.gplay.scrapper) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kermit) + implementation(libs.platform.tools.release.fetcher) + } + + } + + //https://kotlinlang.org/docs/native-objc-interop.html#export-of-kdoc-comments-to-generated-objective-c-headers + targets.withType { + compilations["main"].compileTaskProvider.configure { + compilerOptions { + freeCompilerArgs.add("-Xexport-kdoc") + } + } + } + +} + +android { + namespace = "io.github.kdroidfilter.database.downloader" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + } +} + + +mavenPublishing { + coordinates( + groupId = "io.github.kdroidfilter.database.downloader", + artifactId = "core", + version = version.toString() + ) + + pom { + name.set("KDroid Database Downloader") + description.set("Downloader of the Kdroid Database") + inceptionYear.set("2025") + url.set("https://github.com/kdroidFilter/KDroidDatabase") + + licenses { + license { + name.set("MIT License") + url.set("https://opensource.org/licenses/MIT") + } + } + + developers { + developer { + id.set("kdroidfilter") + name.set("Elie Gambache") + email.set("elyahou.hadass@gmail.com") + } + } + + scm { + connection.set("scm:git:git://github.com/kdroidFilter/KDroidDatabase.git") + developerConnection.set("scm:git:ssh://git@github.com/kdroidFilter/KDroidDatabase.git") + url.set("https://github.com/kdroidFilter/KDroidDatabase") + } + } + + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + + signAllPublications() +} diff --git a/downloader/src/commonMain/kotlin/io/github/kdroidfilter/database/downloader/DatabaseDownloader.kt b/downloader/src/commonMain/kotlin/io/github/kdroidfilter/database/downloader/DatabaseDownloader.kt new file mode 100644 index 0000000..4631eae --- /dev/null +++ b/downloader/src/commonMain/kotlin/io/github/kdroidfilter/database/downloader/DatabaseDownloader.kt @@ -0,0 +1,207 @@ +package io.github.kdroidfilter.database.downloader + +import co.touchlab.kermit.Logger +import io.github.kdroidfilter.platformtools.releasefetcher.github.GitHubReleaseFetcher +import java.io.File +import java.io.FileOutputStream +import java.net.URI + +/** + * Class responsible for downloading KDroid database files from GitHub releases. + */ +class DatabaseDownloader { + private val logger = Logger.withTag("DatabaseDownloader") + + /** + * Downloads the latest store database for a specific language from GitHub releases. + * @param outputDir The directory where the database file will be saved + * @param language The language code (en, fr, he) for which to download the database + * @return Boolean indicating whether the download was successful + */ + suspend fun downloadLatestStoreDatabaseForLanguage(outputDir: String, language: String): Boolean { + try { + logger.i { "🔄 Attempting to download the latest store database for language: $language..." } + val fetcher = GitHubReleaseFetcher(owner = GitHubConstants.OWNER, repo = GitHubConstants.REPO) + val latestRelease = fetcher.getLatestRelease() + + if (latestRelease != null && latestRelease.assets.isNotEmpty()) { + // Look for language-specific database file + val assetName = "store-database-$language.db" + val asset = latestRelease.assets.find { it.name == assetName } + + if (asset != null) { + val outputDirFile = File(outputDir) + if (!outputDirFile.exists()) { + outputDirFile.mkdirs() + } + + val outputDbFile = File(outputDirFile, assetName) + val downloadUrl = asset.browser_download_url + + logger.i { "📥 Downloading $language store database from: $downloadUrl" } + + // Download the file + downloadFile(downloadUrl, outputDbFile.absolutePath) + + // Verify the file was downloaded successfully + if (outputDbFile.exists() && outputDbFile.length() > 0) { + logger.i { + "✅ Store database $language downloaded successfully to ${outputDbFile.absolutePath}" + } + return true + } else { + logger.w { "⚠️ Downloaded file for $language is empty or does not exist" } + return false + } + } else { + logger.w { "⚠️ No store database asset found for language: $language" } + return false + } + } else { + logger.w { "⚠️ No store database assets found in the latest release" } + return false + } + } catch (e: Exception) { + logger.e(e) { "❌ Failed to download store database for $language: ${e.message}" } + return false + } + } + + /** + * Downloads the latest store databases for all three languages (en, fr, he) from GitHub releases. + * @param outputDir The directory where the database files will be saved + * @return Map of language codes to download success status + */ + suspend fun downloadLatestStoreDatabases(outputDir: String): Map { + val languages = listOf("en", "fr", "he") + val results = mutableMapOf() + + try { + logger.i { "🔄 Attempting to download the latest store databases for all languages..." } + val fetcher = GitHubReleaseFetcher(owner = GitHubConstants.OWNER, repo = GitHubConstants.REPO) + val latestRelease = fetcher.getLatestRelease() + + if (latestRelease != null && latestRelease.assets.isNotEmpty()) { + languages.forEach { lang -> + try { + // Look for language-specific database file + val assetName = "store-database-$lang.db" + val asset = latestRelease.assets.find { it.name == assetName } + + if (asset != null) { + val outputDirFile = File(outputDir) + if (!outputDirFile.exists()) { + outputDirFile.mkdirs() + } + + val outputDbFile = File(outputDirFile, assetName) + val downloadUrl = asset.browser_download_url + + logger.i { "📥 Downloading $lang store database from: $downloadUrl" } + + // Download the file + downloadFile(downloadUrl, outputDbFile.absolutePath) + + // Verify the file was downloaded successfully + if (outputDbFile.exists() && outputDbFile.length() > 0) { + logger.i { + "✅ Store database $lang downloaded successfully to ${outputDbFile.absolutePath}" + } + results[lang] = true + } else { + logger.w { "⚠️ Downloaded file for $lang is empty or does not exist" } + results[lang] = false + } + } else { + logger.w { "⚠️ No store database asset found for language: $lang" } + results[lang] = false + } + } catch (e: Exception) { + logger.e(e) { "❌ Failed to download store database for $lang: ${e.message}" } + results[lang] = false + } + } + } else { + logger.w { "⚠️ No store database assets found in the latest release" } + languages.forEach { results[it] = false } + } + } catch (e: Exception) { + logger.e(e) { "❌ Failed to download store databases: ${e.message}" } + languages.forEach { results[it] = false } + } + + return results + } + + /** + * Downloads the latest policies database from GitHub releases. + * @param outputDir The directory where the database file will be saved + * @return Boolean indicating whether the download was successful + */ + suspend fun downloadLatestPoliciesDatabase(outputDir: String): Boolean { + try { + logger.i { "🔄 Attempting to download the latest policies database..." } + val fetcher = GitHubReleaseFetcher(owner = GitHubConstants.OWNER, repo = GitHubConstants.REPO) + val latestRelease = fetcher.getLatestRelease() + + if (latestRelease != null && latestRelease.assets.isNotEmpty()) { + // Look for policies database file + val assetName = "policies-database.db" + val asset = latestRelease.assets.find { it.name == assetName } + + if (asset != null) { + val outputDirFile = File(outputDir) + if (!outputDirFile.exists()) { + outputDirFile.mkdirs() + } + + val outputDbFile = File(outputDirFile, assetName) + val downloadUrl = asset.browser_download_url + + logger.i { "📥 Downloading policies database from: $downloadUrl" } + + // Download the file + downloadFile(downloadUrl, outputDbFile.absolutePath) + + // Verify the file was downloaded successfully + if (outputDbFile.exists() && outputDbFile.length() > 0) { + logger.i { "✅ Policies database downloaded successfully to ${outputDbFile.absolutePath}" } + return true + } else { + logger.w { "⚠️ Downloaded policies database file is empty or does not exist" } + return false + } + } else { + logger.w { "⚠️ No policies database asset found in the latest release" } + return false + } + } else { + logger.w { "⚠️ No assets found in the latest release" } + return false + } + } catch (e: Exception) { + logger.e(e) { "❌ Failed to download policies database: ${e.message}" } + return false + } + } + + /** + * Downloads a file from a URL to a local file path. + * @param url The URL to download from + * @param outputPath The local file path to save the downloaded file + */ + private fun downloadFile(url: String, outputPath: String) { + val outputFile = File(outputPath) + + // Create parent directories if they don't exist + outputFile.parentFile?.mkdirs() + + // Download the file + URI(url).toURL().openStream().use { input -> + FileOutputStream(outputFile).use { output -> + input.copyTo(output) + } + } + } + +} diff --git a/downloader/src/commonMain/kotlin/io/github/kdroidfilter/database/downloader/DatabaseVersionChecker.kt b/downloader/src/commonMain/kotlin/io/github/kdroidfilter/database/downloader/DatabaseVersionChecker.kt new file mode 100644 index 0000000..e643c2a --- /dev/null +++ b/downloader/src/commonMain/kotlin/io/github/kdroidfilter/database/downloader/DatabaseVersionChecker.kt @@ -0,0 +1,45 @@ +package io.github.kdroidfilter.database.downloader + +import co.touchlab.kermit.Logger +import io.github.kdroidfilter.platformtools.releasefetcher.github.GitHubReleaseFetcher + +/** + * Class responsible for checking if the KDroid database version is up to date. + */ +class DatabaseVersionChecker { + private val logger = Logger.withTag("DatabaseVersionChecker") + + /** + * Checks if the database version is up to date compared to the latest release using the release name. + * @param version The version string to check against the latest release name + * @return Boolean indicating whether the provided version is up to date + */ + suspend fun isDatabaseVersionUpToDate(version: String): Boolean { + try { + logger.i { "🔄 Checking if database version $version is up to date using release name..." } + val fetcher = GitHubReleaseFetcher(owner = GitHubConstants.OWNER, repo = GitHubConstants.REPO) + val latestRelease = fetcher.getLatestRelease() + + if (latestRelease != null) { + val latestVersion = latestRelease.name + + val isUpToDate = version == latestVersion + + if (isUpToDate) { + logger.i { "✅ Database version $version is up to date with latest release $latestVersion" } + } else { + logger.i { "⚠️ Database version $version is outdated. Latest release is $latestVersion" } + } + + return isUpToDate + } else { + logger.w { "⚠️ Could not fetch latest release information" } + return false + } + } catch (e: Exception) { + logger.e(e) { "❌ Failed to check database version by release name: ${e.message}" } + return false + } + } + +} diff --git a/downloader/src/commonMain/kotlin/io/github/kdroidfilter/database/downloader/GitHubConstants.kt b/downloader/src/commonMain/kotlin/io/github/kdroidfilter/database/downloader/GitHubConstants.kt new file mode 100644 index 0000000..7ee82ce --- /dev/null +++ b/downloader/src/commonMain/kotlin/io/github/kdroidfilter/database/downloader/GitHubConstants.kt @@ -0,0 +1,16 @@ +package io.github.kdroidfilter.database.downloader + +/** + * Constants for GitHub repository information used across the downloader module. + */ +object GitHubConstants { + /** + * The GitHub repository owner. + */ + const val OWNER = "kdroidFilter" + + /** + * The GitHub repository name. + */ + const val REPO = "KDroidDatabase" +} \ No newline at end of file diff --git a/downloader/src/jvmMain/kotlin/io/github/kdroidfilter/database/downloader/Test.kt b/downloader/src/jvmMain/kotlin/io/github/kdroidfilter/database/downloader/Test.kt new file mode 100644 index 0000000..8439431 --- /dev/null +++ b/downloader/src/jvmMain/kotlin/io/github/kdroidfilter/database/downloader/Test.kt @@ -0,0 +1,9 @@ +package io.github.kdroidfilter.database.downloader + +import kotlinx.coroutines.runBlocking + +fun main() { + runBlocking { + DatabaseVersionChecker().isDatabaseVersionUpToDate("202506151126") + } +} diff --git a/generators/store/build.gradle.kts b/generators/store/build.gradle.kts index 3222e73..89fa7b7 100644 --- a/generators/store/build.gradle.kts +++ b/generators/store/build.gradle.kts @@ -17,6 +17,7 @@ kotlin { jvmMain.dependencies { implementation(project(":core")) implementation(project(":dao")) + implementation(project(":downloader")) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.test) implementation(libs.kotlinx.serialization.json) diff --git a/generators/store/src/jvmMain/kotlin/SqliteStoreBuilder.kt b/generators/store/src/jvmMain/kotlin/SqliteStoreBuilder.kt index 79f1494..d3fde04 100644 --- a/generators/store/src/jvmMain/kotlin/SqliteStoreBuilder.kt +++ b/generators/store/src/jvmMain/kotlin/SqliteStoreBuilder.kt @@ -3,24 +3,23 @@ import co.touchlab.kermit.Logger import com.kdroid.gplayscrapper.core.model.GooglePlayApplicationInfo import com.kdroid.gplayscrapper.services.getGooglePlayApplicationInfo import io.github.kdroidfilter.database.core.AppCategory -import io.github.kdroidfilter.database.store.App_categoriesQueries -import io.github.kdroidfilter.database.store.ApplicationsQueries -import io.github.kdroidfilter.database.store.Database -import io.github.kdroidfilter.database.store.DevelopersQueries -import io.github.kdroidfilter.database.store.VersionQueries -import io.github.kdroidfilter.platformtools.releasefetcher.github.GitHubReleaseFetcher +import io.github.kdroidfilter.database.downloader.DatabaseDownloader +import io.github.kdroidfilter.database.store.* import kotlinx.coroutines.runBlocking -import java.net.URL import java.nio.file.Files import java.nio.file.Path -import java.nio.file.StandardCopyOption import java.time.LocalDateTime import java.time.format.DateTimeFormatter object SqliteStoreBuilder { private val logger = Logger.withTag("SqliteStoreBuilder") - fun buildDatabase(appPoliciesDir: Path, outputDbPath: Path, language: String = "en", country: String = "us") { + fun buildDatabase( + appPoliciesDir: Path, + outputDbPath: Path, + language: String = "en", + country: String = "us" + ) { // Get release name from environment variable or generate timestamp val releaseName = System.getenv("RELEASE_NAME") ?: LocalDateTime.now() .format(DateTimeFormatter.ofPattern("yyyyMMddHHmm")) @@ -97,12 +96,15 @@ object SqliteStoreBuilder { val categoryId = getOrCreateCategory(categoryName, appCategoriesQueries) // Fetch application info from Google Play with specified language and country - val appInfo = runBlocking { - existingApps.getOrPut(packageName) { - runCatching { - getGooglePlayApplicationInfo(packageName, language, country) - }.getOrNull() + val appInfo = existingApps[packageName] ?: try { + runBlocking { + getGooglePlayApplicationInfo(packageName, language, country).also { + existingApps[packageName] = it + } } + } catch (e: Exception) { + logger.w { "⚠️ Failed to fetch info for $packageName: ${e.message}" } + null } if (appInfo != null) { @@ -133,63 +135,12 @@ object SqliteStoreBuilder { * @param baseDbPath The base path used to determine the output directory * @return Map of language codes to download success status */ - private fun downloadLatestDatabases(baseDbPath: Path): Map = runBlocking { - val outputDir = baseDbPath.parent - val languages = listOf("en", "fr", "he") - val results = mutableMapOf() - - try { - logger.i { "🔄 Attempting to download the latest databases for all languages..." } - val fetcher = GitHubReleaseFetcher(owner = "kdroidFilter", repo = "KDroidDatabase") - val latestRelease = fetcher.getLatestRelease() - - if (latestRelease != null && latestRelease.assets.isNotEmpty()) { - languages.forEach { lang -> - try { - // Look for language-specific database file - val assetName = "store-database-$lang.db" - val asset = latestRelease.assets.find { it.name == assetName } - - if (asset != null) { - val outputDbPath = outputDir.resolve(assetName) - val downloadUrl = asset.browser_download_url - - logger.i { "📥 Downloading $lang database from: $downloadUrl" } - - Files.createDirectories(outputDbPath.parent) - - // Download the file - URL(downloadUrl).openStream().use { input -> - Files.copy(input, outputDbPath, StandardCopyOption.REPLACE_EXISTING) - } - - // Verify the file was downloaded successfully - if (Files.exists(outputDbPath) && Files.size(outputDbPath) > 0) { - logger.i { "✅ Database $lang downloaded successfully to $outputDbPath" } - results[lang] = true - } else { - logger.w { "⚠️ Downloaded file for $lang is empty or does not exist" } - results[lang] = false - } - } else { - logger.w { "⚠️ No database asset found for language: $lang" } - results[lang] = false - } - } catch (e: Exception) { - logger.e(e) { "❌ Failed to download database for $lang: ${e.message}" } - results[lang] = false - } - } - } else { - logger.w { "⚠️ No database assets found in the latest release" } - languages.forEach { results[it] = false } - } - } catch (e: Exception) { - logger.e(e) { "❌ Failed to download databases: ${e.message}" } - languages.forEach { results[it] = false } - } + private suspend fun downloadLatestDatabases(baseDbPath: Path): Map { + val outputDir = baseDbPath.parent.toString() - return@runBlocking results + // Use the DatabaseDownloader to download the latest databases + val databaseDownloader = DatabaseDownloader() + return databaseDownloader.downloadLatestStoreDatabases(outputDir) } /** @@ -222,8 +173,8 @@ object SqliteStoreBuilder { val categoryId = getOrCreateCategory(categoryName, appCategoriesQueries) // Fetch application info from Google Play with specified language and country - val appInfo = runBlocking { - existingApps.getOrPut(packageName) { + val appInfo = existingApps.getOrPut(packageName) { + runBlocking { runCatching { getGooglePlayApplicationInfo(packageName, language, country) }.getOrNull() @@ -365,7 +316,7 @@ object SqliteStoreBuilder { /** * Builds or updates databases for all three languages, downloading existing ones first */ - fun buildOrUpdateMultiLanguageDatabases(appPoliciesDir: Path, baseDbPath: Path) { + suspend fun buildOrUpdateMultiLanguageDatabases(appPoliciesDir: Path, baseDbPath: Path) { val outputDir = baseDbPath.parent val languages = mapOf( "en" to "us", diff --git a/generators/store/src/jvmMain/kotlin/SqliteStoreExtractor.kt b/generators/store/src/jvmMain/kotlin/SqliteStoreExtractor.kt index 0c37d46..7453fd8 100644 --- a/generators/store/src/jvmMain/kotlin/SqliteStoreExtractor.kt +++ b/generators/store/src/jvmMain/kotlin/SqliteStoreExtractor.kt @@ -1,3 +1,4 @@ +import kotlinx.coroutines.runBlocking import java.nio.file.Path fun main() { @@ -12,5 +13,7 @@ fun main() { val baseDbPath = projectDir.resolve("build/store-database.db") // Build databases in multiple languages (English, French, Hebrew) - SqliteStoreBuilder.buildOrUpdateMultiLanguageDatabases(policiesDir, baseDbPath) + runBlocking { + SqliteStoreBuilder.buildOrUpdateMultiLanguageDatabases(policiesDir, baseDbPath) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 12bcdd9..f62622d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ ktor = "3.1.3" sqlDelight = "2.1.0" sqlJs = "1.8.0" webPackPlugin = "9.1.0" -gplayscrapper = "0.3.1" +gplayscrapper = "0.3.2" kotlinxBrowserWasmJs = "0.3" [libraries] diff --git a/sample/composeApp/build.gradle.kts b/sample/composeApp/build.gradle.kts index a46679d..8b02f30 100644 --- a/sample/composeApp/build.gradle.kts +++ b/sample/composeApp/build.gradle.kts @@ -23,6 +23,7 @@ kotlin { implementation(project(":core")) implementation(project(":dao")) implementation(project(":localization")) + implementation(project(":downloader")) implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material3) diff --git a/sample/composeApp/src/androidMain/kotlin/sample/app/SqlDriverFactory.kt b/sample/composeApp/src/androidMain/kotlin/sample/app/SqlDriverFactory.kt index f289dbe..2ffaa18 100644 --- a/sample/composeApp/src/androidMain/kotlin/sample/app/SqlDriverFactory.kt +++ b/sample/composeApp/src/androidMain/kotlin/sample/app/SqlDriverFactory.kt @@ -1,33 +1,66 @@ package sample.app import android.content.Context -import android.os.Build -import androidx.annotation.RequiresApi import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver +import co.touchlab.kermit.Logger import io.github.kdroidfilter.database.store.Database import java.nio.file.Path import java.nio.file.Paths import java.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import sample.app.utils.isDatabaseExists +import sample.app.utils.downloadDatabaseIfNotExists private lateinit var applicationContext: Context +private val logger = Logger.withTag("SqlDriverFactory") fun initializeContext(context: Context) { applicationContext = context.applicationContext } actual fun createSqlDriver(): SqlDriver { + val language = getDeviceLanguage() + val dbName = "store-database-${language}.db" + + // Check if database exists with content and download it if it doesn't or is empty + val dbPath = getDatabasePath() + val dbExists = isDatabaseExists(dbPath) + logger.i { "Database exists with content check: $dbExists for path: $dbPath" } + + if (!dbExists) { + logger.i { "Attempting to download database for language: $language" } + // Use runBlocking but with withContext to move the network operation to IO dispatcher + runBlocking { + try { + val downloadSuccess = withContext(Dispatchers.IO) { + downloadDatabaseIfNotExists(dbPath, language) + } + if (!downloadSuccess) { + logger.e { "Failed to download database. Creating a new empty database." } + } + } catch (e: Exception) { + logger.e(e) { "❌ Failed to download store database for $language: ${e.message}" } + } + } + } + val driver = AndroidSqliteDriver( schema = Database.Schema, context = applicationContext, - name = "store-database.db" + name = dbName ) return driver } -@RequiresApi(Build.VERSION_CODES.O) + + + actual fun getDatabasePath(): Path { - val databaseFile = applicationContext.getDatabasePath("store-database.db") + val language = getDeviceLanguage() + val databaseFile = applicationContext.getDatabasePath("store-database-${language}.db") return Paths.get(databaseFile.absolutePath) } @@ -41,4 +74,4 @@ actual fun getDeviceLanguage(): String { "he", "iw" -> "he" // iw is the old code for Hebrew else -> "en" // Default to English for any other language } -} \ No newline at end of file +} diff --git a/sample/composeApp/src/androidMain/kotlin/sample/app/main.kt b/sample/composeApp/src/androidMain/kotlin/sample/app/main.kt index 7ad80ea..7ffcb9d 100644 --- a/sample/composeApp/src/androidMain/kotlin/sample/app/main.kt +++ b/sample/composeApp/src/androidMain/kotlin/sample/app/main.kt @@ -12,7 +12,6 @@ class AppActivity : ComponentActivity() { // Initialize the application context for database access initializeContext(this) - enableEdgeToEdge() setContent { App() } } } diff --git a/sample/composeApp/src/commonMain/kotlin/sample/app/App.kt b/sample/composeApp/src/commonMain/kotlin/sample/app/App.kt index 765842a..da6aa4a 100644 --- a/sample/composeApp/src/commonMain/kotlin/sample/app/App.kt +++ b/sample/composeApp/src/commonMain/kotlin/sample/app/App.kt @@ -46,6 +46,7 @@ import kotlinx.coroutines.withContext import java.nio.file.Path import io.github.kdroidfilter.database.dao.ApplicationsDao import io.github.kdroidfilter.database.dao.AppInfoWithExtras +import io.github.kdroidfilter.database.downloader.DatabaseDownloader import sample.app.ui.AppDetailDialog import sample.app.ui.AppRow import sample.app.ui.SearchScreen @@ -143,6 +144,30 @@ fun App() { MainScope().launch { try { withContext(Dispatchers.IO) { + // Check if database version is up to date + val isUpToDate = sample.app.utils.isDatabaseVersionUpToDate(database) + + // If not up to date, download the new version + if (!isUpToDate) { + logger.i { "Database is not up to date. Downloading new version..." } + val downloader = DatabaseDownloader() + val success = downloader.downloadLatestStoreDatabaseForLanguage( + getDatabasePath().parent.toString(), + getDeviceLanguage() + ) + + if (success) { + message = "Database updated to the latest version!" + } else { + message = "Failed to download the latest database version." + isRefreshing = false + return@withContext + } + } else { + logger.i { "Database is already up to date." } + } + + // Load applications from the database val apps = ApplicationsDao.loadApplicationsFromDatabase( database = database, deviceLanguage = getDeviceLanguage(), @@ -156,7 +181,9 @@ fun App() { ) applications = apps } - message = "Database refreshed successfully!" + if (message == null) { + message = "Database refreshed successfully!" + } } catch (e: Exception) { logger.e { "Refresh error: ${e.message}" } message = "Refresh error: ${e.message}" diff --git a/sample/composeApp/src/commonMain/kotlin/sample/app/utils/DatabaseUtils.kt b/sample/composeApp/src/commonMain/kotlin/sample/app/utils/DatabaseUtils.kt new file mode 100644 index 0000000..03224e2 --- /dev/null +++ b/sample/composeApp/src/commonMain/kotlin/sample/app/utils/DatabaseUtils.kt @@ -0,0 +1,100 @@ +package sample.app.utils + +import co.touchlab.kermit.Logger +import io.github.kdroidfilter.database.dao.VersionDao +import io.github.kdroidfilter.database.downloader.DatabaseDownloader +import io.github.kdroidfilter.database.downloader.DatabaseVersionChecker +import io.github.kdroidfilter.database.store.Database +import kotlinx.coroutines.runBlocking +import java.nio.file.Files +import java.nio.file.Path + +/** + * Checks if the database file exists at the specified path and has a non-zero size. + * @param databasePath The path to the database file + * @return Boolean indicating whether the database file exists and has content + */ +fun isDatabaseExists(databasePath: Path): Boolean { + val logger = Logger.withTag("DatabaseUtils") + try { + val exists = Files.exists(databasePath) + if (!exists) { + logger.w { "⚠️ Database file does not exist at: $databasePath" } + return false + } + + val size = Files.size(databasePath) + val isValid = size > 60000 + + if (isValid) { + logger.i { "✅ Database file exists and has content (${size} bytes) at: $databasePath" } + } else { + logger.w { "⚠️ Database file exists but is empty (0 bytes) at: $databasePath" } + } + + return isValid + } catch (e: Exception) { + logger.e(e) { "❌ Failed to check if database exists or has content: ${e.message}" } + return false + } +} + +/** + * Downloads the database if it doesn't exist at the specified path or has zero size. + * @param databasePath The path where the database should be located + * @param language The language code (en, fr, he) for which to download the database + * @return Boolean indicating whether the database exists with content or was successfully downloaded + */ +fun downloadDatabaseIfNotExists(databasePath: Path, language: String): Boolean { + val logger = Logger.withTag("DatabaseUtils") + + // Check if database exists and has content + if (isDatabaseExists(databasePath)) { + logger.i { "✅ Database already exists with content at: $databasePath" } + return true + } + + logger.i { "🔄 Database does not exist or is empty. Attempting to download..." } + + // Get the parent directory of the database file + val parentDir = databasePath.parent.toString() + + // Download the database + return runBlocking { + val downloader = DatabaseDownloader() + val success = downloader.downloadLatestStoreDatabaseForLanguage(parentDir, language) + + if (success) { + logger.i { "✅ Database downloaded successfully to: $databasePath" } + } else { + logger.e { "❌ Failed to download database to: $databasePath" } + } + + success + } +} + +/** + * Checks if the database version is up to date using the version stored in the database. + * @param database The database instance + * @return Boolean indicating whether the database version is up to date + */ +suspend fun isDatabaseVersionUpToDate(database: Database): Boolean { + val logger = Logger.withTag("DatabaseUtils") + try { + // Get the current version from the database + val currentVersion = VersionDao.getCurrentVersion(database) + + if (currentVersion == null) { + logger.w { "⚠️ No version information found in the database" } + return false + } + + logger.i { "🔄 Checking if database version $currentVersion is up to date using DAO..." } + + return DatabaseVersionChecker().isDatabaseVersionUpToDate(currentVersion) + } catch (e: Exception) { + logger.e(e) { "❌ Failed to check database version using DAO: ${e.message}" } + return false + } +} diff --git a/sample/composeApp/src/jvmMain/kotlin/sample/app/SqlDriverFactory.kt b/sample/composeApp/src/jvmMain/kotlin/sample/app/SqlDriverFactory.kt index e725002..0c762f3 100644 --- a/sample/composeApp/src/jvmMain/kotlin/sample/app/SqlDriverFactory.kt +++ b/sample/composeApp/src/jvmMain/kotlin/sample/app/SqlDriverFactory.kt @@ -2,31 +2,49 @@ package sample.app import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import co.touchlab.kermit.Logger +import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.util.* +import sample.app.utils.isDatabaseExists +import sample.app.utils.downloadDatabaseIfNotExists + +private val logger = Logger.withTag("SqlDriverFactory") actual fun createSqlDriver(): SqlDriver { val dbPath = getDatabasePath() - val driver = JdbcSqliteDriver("jdbc:sqlite:${dbPath.toAbsolutePath()}") + val language = getDeviceLanguage() + + // Check if database exists with content and download it if it doesn't or is empty + val dbExists = isDatabaseExists(dbPath) + logger.i { "Database exists with content check: $dbExists for path: $dbPath" } + + if (!dbExists) { + logger.i { "Attempting to download database for language: $language" } + val downloadSuccess = downloadDatabaseIfNotExists(dbPath, language) + if (!downloadSuccess) { + logger.e { "Failed to download database. Creating a new empty database." } + } + } + val driver = JdbcSqliteDriver("jdbc:sqlite:${dbPath.toAbsolutePath()}") return driver } + + actual fun getDatabasePath(): Path { - // Utiliser le chemin du projet - val projectRoot = System.getProperty("user.dir") - - // Remonter au répertoire parent si nous sommes dans sample/ - val rootDir = if (projectRoot.endsWith("sample")) { - Paths.get(projectRoot).parent - } else { - Paths.get(projectRoot) - } + // Use a production-ready directory + val userHome = System.getProperty("user.home") + val appDataDir = Paths.get(userHome, ".kdroid-database") + + // Create directory if it doesn't exist + Files.createDirectories(appDataDir) - // Chemin vers la base de données générée + // Use language-specific database file val language = getDeviceLanguage() - return rootDir.resolve("../../generators/store/build/store-database-${language}.db") + return appDataDir.resolve("store-database-${language}.db") } actual fun getDeviceLanguage(): String { diff --git a/settings.gradle.kts b/settings.gradle.kts index d92004a..432f684 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,4 +34,5 @@ include(":dao") include(":generators:policies") include(":generators:store") include(":localization") +include(":downloader") include(":sample:composeApp")