From b43f59964d8a5f91d8c8ce98ef5cc0b30168eecb Mon Sep 17 00:00:00 2001 From: LooKeR Date: Thu, 16 Jul 2026 19:07:50 +0530 Subject: [PATCH 01/19] build: Add sqldelight dependency --- app/build.gradle.kts | 15 +++++++++++++++ gradle/libs.versions.toml | 7 +++++++ 2 files changed, 22 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 753ec8bb5..450f3308d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.compose) + alias(libs.plugins.sqldelight) } android { @@ -98,6 +99,19 @@ ksp { arg("room.generateKotlin", "true") } +sqldelight { + databases { + create("DroidifyDb") { + packageName.set("com.looker.droidify.data.local.sql") + // Lowest dialect SQLDelight offers; still newer than minSdk 23's + // SQLite 3.8.10, so avoid post-3.8 syntax (UPSERT, row values, ...). + dialect(libs.sqldelight.dialect.sqlite318) + schemaOutputDirectory.set(file("src/main/sqldelight/databases")) + verifyMigrations.set(true) + } + } +} + kotlin { compilerOptions { freeCompilerArgs.addAll("-Xcontext-parameters") @@ -141,6 +155,7 @@ dependencies { implementation(libs.okhttp) implementation(libs.bundles.room) ksp(libs.room.compiler) + implementation(libs.bundles.sqldelight) implementation(libs.work.ktx) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a5c7947e5..b6b829d5b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,6 +26,7 @@ okhttp = "5.1.0" libsu = "6.0.0" room = "2.8.4" shizuku = "13.0.0" +sqldelight = "2.3.2" image-viewer = "1.0.1" junit-jupiter = "6.0.3" robolectric = "4.16.1" @@ -81,6 +82,10 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } room-test = { group = "androidx.room", name = "room-testing", version.ref = "room" } +sqldelight-android-driver = { group = "app.cash.sqldelight", name = "android-driver", version.ref = "sqldelight" } +sqldelight-dialect-sqlite318 = { group = "app.cash.sqldelight", name = "sqlite-3-18-dialect", version.ref = "sqldelight" } +sqldelight-coroutines = { group = "app.cash.sqldelight", name = "coroutines-extensions", version.ref = "sqldelight" } +sqldelight-primitive-adapters = { group = "app.cash.sqldelight", name = "primitive-adapters", version.ref = "sqldelight" } shizuku-api = { group = "dev.rikka.shizuku", name = "api", version.ref = "shizuku" } shizuku-provider = { group = "dev.rikka.shizuku", name = "provider", version.ref = "shizuku" } image-viewer = { module = "com.github.stfalcon-studio:StfalconImageViewer", version.ref = "image-viewer" } @@ -113,10 +118,12 @@ hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } [bundles] room = ["room-runtime", "room-ktx"] +sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-primitive-adapters"] shizuku = ["shizuku-provider", "shizuku-api"] coroutines = ["coroutines-core", "coroutines-android", "coroutines-guava"] coil = ["coil-core", "coil-compose", "coil-network"] From a00e9f2dca0ab7d4aa122825f2588e3a8b858966 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Fri, 17 Jul 2026 11:45:34 +0530 Subject: [PATCH 02/19] feat: Add SQLDelight Schema Improved compared to room but there might be some optimizations possible yet --- .../droidify/data/local/sql/AntiFeature.sq | 24 +++++++++ .../com/looker/droidify/data/local/sql/App.sq | 33 ++++++++++++ .../droidify/data/local/sql/AppMetadata.sq | 43 ++++++++++++++++ .../droidify/data/local/sql/Category.sq | 24 +++++++++ .../droidify/data/local/sql/Repository.sq | 47 ++++++++++++++++++ .../looker/droidify/data/local/sql/Version.sq | 30 +++++++++++ app/src/main/sqldelight/databases/1.db | Bin 0 -> 131072 bytes 7 files changed, 201 insertions(+) create mode 100644 app/src/main/sqldelight/com/looker/droidify/data/local/sql/AntiFeature.sq create mode 100644 app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq create mode 100644 app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq create mode 100644 app/src/main/sqldelight/com/looker/droidify/data/local/sql/Category.sq create mode 100644 app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq create mode 100644 app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq create mode 100644 app/src/main/sqldelight/databases/1.db diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AntiFeature.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AntiFeature.sq new file mode 100644 index 000000000..db3c4518f --- /dev/null +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AntiFeature.sq @@ -0,0 +1,24 @@ +import com.looker.droidify.sync.v2.model.AntiFeatureReason; +import kotlin.Int; + +CREATE TABLE anti_feature ( + icon TEXT, + name TEXT NOT NULL, + description TEXT, + locale TEXT NOT NULL, + tag TEXT NOT NULL, + PRIMARY KEY(tag, locale) +) WITHOUT ROWID; + +CREATE TABLE anti_features_app_relation ( + tag TEXT NOT NULL, + reason TEXT AS AntiFeatureReason NOT NULL, + versionId INTEGER AS Int NOT NULL REFERENCES version(id) ON DELETE CASCADE, + PRIMARY KEY(versionId, tag) +) WITHOUT ROWID; + +CREATE TABLE anti_feature_repo_relation ( + repoId INTEGER AS Int NOT NULL REFERENCES repository(id) ON DELETE CASCADE, + tag TEXT NOT NULL, + PRIMARY KEY(repoId, tag) +) WITHOUT ROWID; diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq new file mode 100644 index 000000000..e3ca293ed --- /dev/null +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq @@ -0,0 +1,33 @@ +import kotlin.Int; + +CREATE TABLE app ( + added INTEGER NOT NULL, + lastUpdated INTEGER NOT NULL, + preferredSigner TEXT, + packageName TEXT NOT NULL, + authorId INTEGER AS Int NOT NULL REFERENCES author(id) ON DELETE RESTRICT, + repoId INTEGER AS Int NOT NULL REFERENCES repository(id) ON DELETE CASCADE, + id INTEGER AS Int NOT NULL PRIMARY KEY +); + +CREATE UNIQUE INDEX index_app_packageName_repoId ON app(packageName, repoId); +CREATE INDEX index_app_repoId ON app(repoId); +CREATE INDEX index_app_authorId ON app(authorId); + +CREATE TABLE localized_app ( + appId INTEGER AS Int NOT NULL REFERENCES app(id) ON DELETE CASCADE, + locale TEXT NOT NULL, + name TEXT, + summary TEXT, + iconName TEXT, + iconSha256 TEXT, + iconSize INTEGER, + description TEXT, + PRIMARY KEY(appId, locale), + CHECK ( + name IS NOT NULL + OR summary IS NOT NULL + OR iconName IS NOT NULL + OR description IS NOT NULL + ) +); diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq new file mode 100644 index 000000000..1151a08cd --- /dev/null +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq @@ -0,0 +1,43 @@ +import kotlin.Int; + +CREATE TABLE author ( + email TEXT NOT NULL DEFAULT '', + name TEXT NOT NULL DEFAULT '', + website TEXT NOT NULL DEFAULT '', + id INTEGER AS Int NOT NULL PRIMARY KEY +); + +CREATE UNIQUE INDEX index_author_email_name_website ON author(email, name, website); + +CREATE TABLE links ( + license TEXT, + changelog TEXT, + issueTracker TEXT, + translation TEXT, + sourceCode TEXT, + webSite TEXT, + appId INTEGER AS Int NOT NULL PRIMARY KEY REFERENCES app(id) ON DELETE CASCADE +); + +CREATE TABLE graphic ( + url TEXT NOT NULL, + type INTEGER AS Int NOT NULL, + locale TEXT NOT NULL, + appId INTEGER AS Int NOT NULL REFERENCES app(id) ON DELETE CASCADE, + PRIMARY KEY(appId, locale, type) +) WITHOUT ROWID; + +CREATE TABLE screenshot ( + path TEXT NOT NULL, + type INTEGER AS Int NOT NULL, + locale TEXT NOT NULL, + appId INTEGER AS Int NOT NULL REFERENCES app(id) ON DELETE CASCADE, + PRIMARY KEY(appId, locale, type, path) +) WITHOUT ROWID; + +CREATE TABLE donate ( + type INTEGER AS Int NOT NULL, + value TEXT NOT NULL, + appId INTEGER AS Int NOT NULL REFERENCES app(id) ON DELETE CASCADE, + PRIMARY KEY(appId, type, value) +) WITHOUT ROWID; diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Category.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Category.sq new file mode 100644 index 000000000..23f0f57b9 --- /dev/null +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Category.sq @@ -0,0 +1,24 @@ +import kotlin.Int; + +CREATE TABLE category ( + icon TEXT, + name TEXT NOT NULL, + description TEXT, + locale TEXT NOT NULL, + defaultName TEXT NOT NULL, + PRIMARY KEY(defaultName, locale) +) WITHOUT ROWID; + +CREATE TABLE category_app_relation ( + appId INTEGER AS Int NOT NULL REFERENCES app(id) ON DELETE CASCADE, + defaultName TEXT NOT NULL, + PRIMARY KEY(appId, defaultName) +) WITHOUT ROWID; + +CREATE INDEX index_category_app_relation_defaultName ON category_app_relation(defaultName); + +CREATE TABLE category_repo_relation ( + repoId INTEGER AS Int NOT NULL REFERENCES repository(id) ON DELETE CASCADE, + defaultName TEXT NOT NULL, + PRIMARY KEY(repoId, defaultName) +) WITHOUT ROWID; diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq new file mode 100644 index 000000000..77edbaf19 --- /dev/null +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq @@ -0,0 +1,47 @@ +import com.looker.droidify.data.encryption.Encrypted; +import com.looker.droidify.data.model.Fingerprint; +import kotlin.Boolean; +import kotlin.Int; + +CREATE TABLE repository ( + address TEXT NOT NULL, + webBaseUrl TEXT, + fingerprint TEXT AS Fingerprint NOT NULL, + timestamp INTEGER, + id INTEGER AS Int NOT NULL PRIMARY KEY +); + +CREATE UNIQUE INDEX index_repository_address ON repository(address); + +CREATE TABLE localized_repo ( + repoId INTEGER AS Int NOT NULL REFERENCES repository(id) ON DELETE CASCADE, + locale TEXT NOT NULL, + name TEXT, + description TEXT, + iconName TEXT, + iconSha256 TEXT, + iconSize INTEGER, + PRIMARY KEY(repoId, locale), + CHECK ( + name IS NOT NULL + OR description IS NOT NULL + OR iconName IS NOT NULL + ) +); + +CREATE TABLE mirror ( + url TEXT NOT NULL, + countryCode TEXT, + isPrimary INTEGER AS Boolean NOT NULL, + repoId INTEGER AS Int NOT NULL REFERENCES repository(id) ON DELETE CASCADE, + PRIMARY KEY(repoId, url) +) WITHOUT ROWID; + +CREATE UNIQUE INDEX index_mirror_primary ON mirror(repoId) WHERE isPrimary = 1; + +CREATE TABLE authentication ( + password TEXT AS Encrypted NOT NULL, + username TEXT NOT NULL, + initializationVector BLOB NOT NULL, + repoId INTEGER AS Int NOT NULL PRIMARY KEY REFERENCES repository(id) ON DELETE CASCADE +); diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq new file mode 100644 index 000000000..fc96b8dfc --- /dev/null +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq @@ -0,0 +1,30 @@ +import com.looker.droidify.sync.v2.model.LocalizedString; +import com.looker.droidify.sync.v2.model.PermissionV2; +import kotlin.Int; +import kotlin.String; +import kotlin.collections.List; + +CREATE TABLE version ( + added INTEGER NOT NULL, + whatsNew TEXT AS LocalizedString NOT NULL, + versionName TEXT NOT NULL, + versionCode INTEGER NOT NULL, + maxSdkVersion INTEGER AS Int, + minSdkVersion INTEGER AS Int NOT NULL, + targetSdkVersion INTEGER AS Int NOT NULL, + features TEXT AS List NOT NULL, + nativeCode TEXT AS List NOT NULL, + permissions TEXT AS List NOT NULL, + permissionsSdk23 TEXT AS List NOT NULL, + apkName TEXT NOT NULL, + apkSha256 TEXT NOT NULL, + apkSize INTEGER NOT NULL, + srcName TEXT, + srcSha256 TEXT, + srcSize INTEGER, + appId INTEGER AS Int NOT NULL REFERENCES app(id) ON DELETE CASCADE, + id INTEGER AS Int NOT NULL PRIMARY KEY +); + +CREATE UNIQUE INDEX index_version_appId_apkSha256 ON version(appId, apkSha256); +CREATE INDEX index_version_appId ON version(appId); diff --git a/app/src/main/sqldelight/databases/1.db b/app/src/main/sqldelight/databases/1.db new file mode 100644 index 0000000000000000000000000000000000000000..7224a7b9f0c5ce50a19a941e74e5c2514ab0bd75 GIT binary patch literal 131072 zcmeI5TW=f3700={tt3mnL{2ug;&z?Fkcg-SY$Gv@#z1L$9TS$Nn4}^bMnH_YBWWRC z*1Jo~%B2M*I6xi>^d+y&LtdH>)5pB@C10WMX@Snp49VG>?mxxLZTx^elkw*N37PaD*) zdz7~Pt#&YYccxgj3RTOj7H%(F=AhhMoHp5yOM)%4YCWo&E353!+Vb)eFAsKIYL-^2 z)<;%3R@G?NNrR?!BCcDq6TQr`b=N9eD@ChfvKEVuoi|rk%sbYy#nx3UREmW=mYDV9 za_K>#{Mfv2JznI4Etz7rB{N*m5?^k9I&ZF*s`pmcs%Ck0y>zGR&+5kfytaRfQ`q!~ zyXDm7uhS9zvQUcec#Sj}ze?>Xs%Lb=FtmM(t8O^0ZJ)o)MilcRskh{Tn zsS{0@F2AR%zbO)}oRlq~2xS^RpVp0=H?{q9Ttl4%bhGX4);#LA*^6MDUV18`F};n{ z9FBI#ZVa0+L2T}WHl%`CNx6u1)I3rAu*p_pCRSs!K|7>XGf1bW*V*JBxwJXkME0UMFEKwtyv~S2hN-?y^gAUpJqET$` zm#s>*Tq?>8?#R-KL(`*|;~G!rzb2fjj5PAsNQAB|do|k{1$H1V8`;KmY_l00ck) z1V8`;Kwv-s_x}M4yaNFc009sH0T2KI5C8!X009sHfs;yLIuwS#|9?`=3tb=p0w4ea zAOHd&00JNY0w4eaAixRW{trh00T2KI5C8!X009sH0T2KI5CDObPXPD-lW$`v1OX5L z0T2KI5C8!X009sH0T2KI-2dSNAOHd&00JNY0w4eaAOHd&00JOz@(JMnfAVb%g&+U| zAOHd&00JNY0w4eaAOHd&fcrmu00ck)1V8`;KmY_l00ck)1V8`;PCfzL|4+V+p%4T> z00ck)1V8`;KmY_l00ck)1ZG17`2YXl2p|9gAOHd&00JNY0w4eaAOHd&aH0v|{(qv) z3vnO-0w4eaAOHd&00JNY0w4eaAixRW@BhOQKmY_l00ck)1V8`;KmY_l00cnb1-MD;N+kY64 zrw!`YJxW{tRy!EHJ5wxMg{oy%3%8dob5L$BPMhqG%Koe^ zFE8=AgI$-JrIo7nkyVaWHQII3plo4@PQ-OfcA}SAw(eSGYo%yaOx9x2vGeBYih0Lc zwwU%}p;9c|vBaz&mrD-{<;UiI>+vEVY{?X}Et%nhmN=69blzMqRqw5?Rn7A1dg)Hr zpVf`|d2Rm|C%x$rcgv~EU#BCCWuXMz@fv9|ewEr&RL|&!VQBjnSKV-0+dhApjVR_t zQg6elGZz%56Ts_Rq_s&K?M==>lsUfNp;eF6x2dNT2Oeqp4H7u*mQw1sJ6@d@+qOFD z4t-j20<|Q=GZcHjl9x>9yMA=k6R$H7-J(nir%mF-OwIi7ZeeY? zYA!4Y*)7tHId!55)8+Sc^*2SLm6Ngs6roJR=hM1z^QN|cj%%osfNr+E-I_<;HhU3_ z(@Rf9G^V$an#0i!*^OZnCWy^_(5AHM2I(|{l?0#Uo)vR0nM#+W-?pc8R|f zLHm$cAX^<22voQ456VvF6fE5*N}5BcASN=U|FZ6%!B7>*nq<2B{_RoSSXj`$ zDsrRqZRUJN?a<(n4`UI1kt!+hwL?|*9`DBr;n9Al*(Bbsa(0KYr61Krqp+y5MXtU7 zL9AVc>FZ^72ZbCJA=lQ2D0&s|S;hNOdzj<$MV2akm*vT4Sg~3*dn?YU?k_*1UX6+I zCTt(y9Gfs6END5WWz!e_vj#iZYb~iUp*v}( zog5{LpseOOZu*U=NnTX!?N3CUjqZ}B(@IvwOcW4rlLm)2lLxAwCZWah^5=szJT`$# zy(Y`**&&eHS*d1=xOP2l0^)9G@nKa;$cRlHRw|(*R_lB9{;0yuScx*&piCJ<`t|GH zH4pW}S^b~6bGeP&^q;3*O#WT}Vf^3v-^Ttk`rpwnM&BLz$H(24GBZAK$FRu~X_Neapg$QX8rxh3iXgw5Uv%e|TZSxH_k?oy-mjL7GMN@T_20 z&yvmFzmqb#+JtjpPd<8G^7r4KFn&L$(b$k8#@k+M*a3Muaq+K|NED-mp z{9oSGjgRKFZa!;hHi3*fvg6y?!Xk4GIwodaVvncfp!x0>=XE2W*Lrz9Gd5*+7v?Q2 ziiUO^d@|(B;wfIVPM%CI@%^2)XQxHhj!(UCyQS4REhlhD!}*L)`w6YHySjOMdG&TI zj!M6XpH8Ndjj7L9!sZ9v``IIkXLI~9i+rTkbUd%k&XDl+t%zJvCS_(kPph{(t-#xj zJtT7ck3ENHr151Ral)ZMSQMa^Ot#X*Rc`;%Id1Zoomf^Mb{Zsr;ivNv2GKgnLd?`l z2Kx5;Ic5+X_~@zL(f1nBP|bJ4hqI!zpCw0&5{E3${1c~a8auI|ycquHbaiN3ex%nPLuipX}UdEk!DL?1k~NkcIf&Uu|uc0 z5Sv9Mc6;EjXCnecnOyt0{r~vb3M&Nx5C8!X009sH0T2KI5C8!X0D)l%;NSlnmK^$l z00@8p2!H?xfB*=900@8p2!O!xA%Oe;@v#+F3IZSi0w4eaAOHd&00JNY0w4ea!xF&X z{~wkd`hfrlfB*=900@8p2!H?xfB*=9!0{n~@BfdFt*}xM009sH0T2KI5C8!X009sH z0T39L0Pg?8l0!cb009sH0T2KI5C8!X009sH0T4Jo1aSX9KDNS2K>!3m00ck)1V8`; zKmY_l00cl_SOU2J4@(aHKmY_l00ck)1V8`;KmY_l00cnb_z=MT|M=JnD+K`%009sH z0T2KI5C8!X009sHfnf>Y{y!`^^aBA9009sH0T2KI5C8!X009sHf#X8}_y6N#E36a* zKmY_l00ck)1V8`;KmY_l00f35fcyWj Date: Fri, 17 Jul 2026 13:02:59 +0530 Subject: [PATCH 03/19] refactor: Use Long as table keys so we do not have to use adapters later --- .../com/looker/droidify/data/local/sql/AntiFeature.sq | 5 ++--- .../com/looker/droidify/data/local/sql/App.sq | 10 ++++------ .../com/looker/droidify/data/local/sql/AppMetadata.sq | 10 +++++----- .../com/looker/droidify/data/local/sql/Category.sq | 6 ++---- .../com/looker/droidify/data/local/sql/Repository.sq | 9 ++++----- .../com/looker/droidify/data/local/sql/Version.sq | 4 ++-- 6 files changed, 19 insertions(+), 25 deletions(-) diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AntiFeature.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AntiFeature.sq index db3c4518f..0105aa49c 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AntiFeature.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AntiFeature.sq @@ -1,5 +1,4 @@ import com.looker.droidify.sync.v2.model.AntiFeatureReason; -import kotlin.Int; CREATE TABLE anti_feature ( icon TEXT, @@ -13,12 +12,12 @@ CREATE TABLE anti_feature ( CREATE TABLE anti_features_app_relation ( tag TEXT NOT NULL, reason TEXT AS AntiFeatureReason NOT NULL, - versionId INTEGER AS Int NOT NULL REFERENCES version(id) ON DELETE CASCADE, + versionId INTEGER NOT NULL REFERENCES version(id) ON DELETE CASCADE, PRIMARY KEY(versionId, tag) ) WITHOUT ROWID; CREATE TABLE anti_feature_repo_relation ( - repoId INTEGER AS Int NOT NULL REFERENCES repository(id) ON DELETE CASCADE, + repoId INTEGER NOT NULL REFERENCES repository(id) ON DELETE CASCADE, tag TEXT NOT NULL, PRIMARY KEY(repoId, tag) ) WITHOUT ROWID; diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq index e3ca293ed..7fc2274f1 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq @@ -1,13 +1,11 @@ -import kotlin.Int; - CREATE TABLE app ( added INTEGER NOT NULL, lastUpdated INTEGER NOT NULL, preferredSigner TEXT, packageName TEXT NOT NULL, - authorId INTEGER AS Int NOT NULL REFERENCES author(id) ON DELETE RESTRICT, - repoId INTEGER AS Int NOT NULL REFERENCES repository(id) ON DELETE CASCADE, - id INTEGER AS Int NOT NULL PRIMARY KEY + authorId INTEGER NOT NULL REFERENCES author(id) ON DELETE RESTRICT, + repoId INTEGER NOT NULL REFERENCES repository(id) ON DELETE CASCADE, + id INTEGER NOT NULL PRIMARY KEY ); CREATE UNIQUE INDEX index_app_packageName_repoId ON app(packageName, repoId); @@ -15,7 +13,7 @@ CREATE INDEX index_app_repoId ON app(repoId); CREATE INDEX index_app_authorId ON app(authorId); CREATE TABLE localized_app ( - appId INTEGER AS Int NOT NULL REFERENCES app(id) ON DELETE CASCADE, + appId INTEGER NOT NULL REFERENCES app(id) ON DELETE CASCADE, locale TEXT NOT NULL, name TEXT, summary TEXT, diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq index 1151a08cd..8ba5776bf 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq @@ -4,7 +4,7 @@ CREATE TABLE author ( email TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '', website TEXT NOT NULL DEFAULT '', - id INTEGER AS Int NOT NULL PRIMARY KEY + id INTEGER NOT NULL PRIMARY KEY ); CREATE UNIQUE INDEX index_author_email_name_website ON author(email, name, website); @@ -16,14 +16,14 @@ CREATE TABLE links ( translation TEXT, sourceCode TEXT, webSite TEXT, - appId INTEGER AS Int NOT NULL PRIMARY KEY REFERENCES app(id) ON DELETE CASCADE + appId INTEGER NOT NULL PRIMARY KEY REFERENCES app(id) ON DELETE CASCADE ); CREATE TABLE graphic ( url TEXT NOT NULL, type INTEGER AS Int NOT NULL, locale TEXT NOT NULL, - appId INTEGER AS Int NOT NULL REFERENCES app(id) ON DELETE CASCADE, + appId INTEGER NOT NULL REFERENCES app(id) ON DELETE CASCADE, PRIMARY KEY(appId, locale, type) ) WITHOUT ROWID; @@ -31,13 +31,13 @@ CREATE TABLE screenshot ( path TEXT NOT NULL, type INTEGER AS Int NOT NULL, locale TEXT NOT NULL, - appId INTEGER AS Int NOT NULL REFERENCES app(id) ON DELETE CASCADE, + appId INTEGER NOT NULL REFERENCES app(id) ON DELETE CASCADE, PRIMARY KEY(appId, locale, type, path) ) WITHOUT ROWID; CREATE TABLE donate ( type INTEGER AS Int NOT NULL, value TEXT NOT NULL, - appId INTEGER AS Int NOT NULL REFERENCES app(id) ON DELETE CASCADE, + appId INTEGER NOT NULL REFERENCES app(id) ON DELETE CASCADE, PRIMARY KEY(appId, type, value) ) WITHOUT ROWID; diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Category.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Category.sq index 23f0f57b9..5c005b57c 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Category.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Category.sq @@ -1,5 +1,3 @@ -import kotlin.Int; - CREATE TABLE category ( icon TEXT, name TEXT NOT NULL, @@ -10,7 +8,7 @@ CREATE TABLE category ( ) WITHOUT ROWID; CREATE TABLE category_app_relation ( - appId INTEGER AS Int NOT NULL REFERENCES app(id) ON DELETE CASCADE, + appId INTEGER NOT NULL REFERENCES app(id) ON DELETE CASCADE, defaultName TEXT NOT NULL, PRIMARY KEY(appId, defaultName) ) WITHOUT ROWID; @@ -18,7 +16,7 @@ CREATE TABLE category_app_relation ( CREATE INDEX index_category_app_relation_defaultName ON category_app_relation(defaultName); CREATE TABLE category_repo_relation ( - repoId INTEGER AS Int NOT NULL REFERENCES repository(id) ON DELETE CASCADE, + repoId INTEGER NOT NULL REFERENCES repository(id) ON DELETE CASCADE, defaultName TEXT NOT NULL, PRIMARY KEY(repoId, defaultName) ) WITHOUT ROWID; diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq index 77edbaf19..2c37d940b 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq @@ -1,20 +1,19 @@ import com.looker.droidify.data.encryption.Encrypted; import com.looker.droidify.data.model.Fingerprint; import kotlin.Boolean; -import kotlin.Int; CREATE TABLE repository ( address TEXT NOT NULL, webBaseUrl TEXT, fingerprint TEXT AS Fingerprint NOT NULL, timestamp INTEGER, - id INTEGER AS Int NOT NULL PRIMARY KEY + id INTEGER NOT NULL PRIMARY KEY ); CREATE UNIQUE INDEX index_repository_address ON repository(address); CREATE TABLE localized_repo ( - repoId INTEGER AS Int NOT NULL REFERENCES repository(id) ON DELETE CASCADE, + repoId INTEGER NOT NULL REFERENCES repository(id) ON DELETE CASCADE, locale TEXT NOT NULL, name TEXT, description TEXT, @@ -33,7 +32,7 @@ CREATE TABLE mirror ( url TEXT NOT NULL, countryCode TEXT, isPrimary INTEGER AS Boolean NOT NULL, - repoId INTEGER AS Int NOT NULL REFERENCES repository(id) ON DELETE CASCADE, + repoId INTEGER NOT NULL REFERENCES repository(id) ON DELETE CASCADE, PRIMARY KEY(repoId, url) ) WITHOUT ROWID; @@ -43,5 +42,5 @@ CREATE TABLE authentication ( password TEXT AS Encrypted NOT NULL, username TEXT NOT NULL, initializationVector BLOB NOT NULL, - repoId INTEGER AS Int NOT NULL PRIMARY KEY REFERENCES repository(id) ON DELETE CASCADE + repoId INTEGER NOT NULL PRIMARY KEY REFERENCES repository(id) ON DELETE CASCADE ); diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq index fc96b8dfc..9f9ac95d6 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq @@ -22,8 +22,8 @@ CREATE TABLE version ( srcName TEXT, srcSha256 TEXT, srcSize INTEGER, - appId INTEGER AS Int NOT NULL REFERENCES app(id) ON DELETE CASCADE, - id INTEGER AS Int NOT NULL PRIMARY KEY + appId INTEGER NOT NULL REFERENCES app(id) ON DELETE CASCADE, + id INTEGER NOT NULL PRIMARY KEY ); CREATE UNIQUE INDEX index_version_appId_apkSha256 ON version(appId, apkSha256); From bd41fa84f6eca8d159b30da2dd9bc794879e5756 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Fri, 17 Jul 2026 14:29:16 +0530 Subject: [PATCH 04/19] refactor: Make SHA256 BLOB in database --- .../looker/droidify/data/model/Fingerprint.kt | 4 +++- .../com/looker/droidify/data/local/sql/App.sq | 4 ++-- .../droidify/data/local/sql/Repository.sq | 4 ++-- .../looker/droidify/data/local/sql/Version.sq | 4 ++-- app/src/main/sqldelight/databases/1.db | Bin 131072 -> 131072 bytes 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/com/looker/droidify/data/model/Fingerprint.kt b/app/src/main/kotlin/com/looker/droidify/data/model/Fingerprint.kt index cd680386d..e9de2a2b9 100644 --- a/app/src/main/kotlin/com/looker/droidify/data/model/Fingerprint.kt +++ b/app/src/main/kotlin/com/looker/droidify/data/model/Fingerprint.kt @@ -26,13 +26,15 @@ value class Fingerprint(val value: String) { } } +fun Fingerprint(blob: ByteArray) = Fingerprint(blob.hex()) + suspend inline fun JarEntry.fingerprint(): Fingerprint? = withContext(Dispatchers.IO) { codeSignerOrNull?.certificateOrNull?.fingerprint() } inline fun Certificate.fingerprint(): Fingerprint? { val bytes = this.encoded.takeIf { it.size >= 256 } ?: return null - return Fingerprint(sha256(bytes).hex().uppercase()).takeIf { it.isValid } + return Fingerprint(sha256(bytes).hex()).takeIf { it.isValid } } inline fun ByteArray.hex(): String = joinToString(separator = "") { byte -> diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq index 7fc2274f1..91a10eeeb 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq @@ -1,7 +1,7 @@ CREATE TABLE app ( added INTEGER NOT NULL, lastUpdated INTEGER NOT NULL, - preferredSigner TEXT, + preferredSigner BLOB, packageName TEXT NOT NULL, authorId INTEGER NOT NULL REFERENCES author(id) ON DELETE RESTRICT, repoId INTEGER NOT NULL REFERENCES repository(id) ON DELETE CASCADE, @@ -18,7 +18,7 @@ CREATE TABLE localized_app ( name TEXT, summary TEXT, iconName TEXT, - iconSha256 TEXT, + iconSha256 BLOB, iconSize INTEGER, description TEXT, PRIMARY KEY(appId, locale), diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq index 2c37d940b..cbdaf9e6b 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq @@ -5,7 +5,7 @@ import kotlin.Boolean; CREATE TABLE repository ( address TEXT NOT NULL, webBaseUrl TEXT, - fingerprint TEXT AS Fingerprint NOT NULL, + fingerprint BLOB AS Fingerprint NOT NULL, timestamp INTEGER, id INTEGER NOT NULL PRIMARY KEY ); @@ -18,7 +18,7 @@ CREATE TABLE localized_repo ( name TEXT, description TEXT, iconName TEXT, - iconSha256 TEXT, + iconSha256 BLOB, iconSize INTEGER, PRIMARY KEY(repoId, locale), CHECK ( diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq index 9f9ac95d6..42f837b9d 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq @@ -17,10 +17,10 @@ CREATE TABLE version ( permissions TEXT AS List NOT NULL, permissionsSdk23 TEXT AS List NOT NULL, apkName TEXT NOT NULL, - apkSha256 TEXT NOT NULL, + apkSha256 BLOB NOT NULL, apkSize INTEGER NOT NULL, srcName TEXT, - srcSha256 TEXT, + srcSha256 BLOB, srcSize INTEGER, appId INTEGER NOT NULL REFERENCES app(id) ON DELETE CASCADE, id INTEGER NOT NULL PRIMARY KEY diff --git a/app/src/main/sqldelight/databases/1.db b/app/src/main/sqldelight/databases/1.db index 7224a7b9f0c5ce50a19a941e74e5c2514ab0bd75..cc223b7831e8af213e6cdc39488a87d4e5934ffb 100644 GIT binary patch delta 120 zcmZo@;Am*z*zli=#mUFtY5EH>Mw!j>+;xm#?&gI&6PduQ$!7eX5TR!Nn%_jK-QLX4 Wn8*%Ryd7w8HpI~F;S!81p928UK_s&P delta 120 zcmZo@;Am*z*zli=CB!u%Wcmv+Mw!j>+;xm#?&gI&6Pc!ekYW^_?9Oii5o+eI`AwwS Z?alm*iR@s-+kpmWLk!&>F2T6+IRIaxCkp@o From aba072e27579258da6c0852f6f35c93b4307a081 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Fri, 17 Jul 2026 15:01:22 +0530 Subject: [PATCH 05/19] refactor: Normalize permissions table --- .../looker/droidify/data/local/sql/Version.sq | 14 +++++++++++--- app/src/main/sqldelight/databases/1.db | Bin 131072 -> 135168 bytes 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq index 42f837b9d..189264f30 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq @@ -1,5 +1,4 @@ import com.looker.droidify.sync.v2.model.LocalizedString; -import com.looker.droidify.sync.v2.model.PermissionV2; import kotlin.Int; import kotlin.String; import kotlin.collections.List; @@ -14,8 +13,6 @@ CREATE TABLE version ( targetSdkVersion INTEGER AS Int NOT NULL, features TEXT AS List NOT NULL, nativeCode TEXT AS List NOT NULL, - permissions TEXT AS List NOT NULL, - permissionsSdk23 TEXT AS List NOT NULL, apkName TEXT NOT NULL, apkSha256 BLOB NOT NULL, apkSize INTEGER NOT NULL, @@ -28,3 +25,14 @@ CREATE TABLE version ( CREATE UNIQUE INDEX index_version_appId_apkSha256 ON version(appId, apkSha256); CREATE INDEX index_version_appId ON version(appId); + +CREATE TABLE permission ( + name TEXT NOT NULL, + maxSdkVersion INTEGER AS Int, + versionId INTEGER NOT NULL REFERENCES version(id) ON DELETE CASCADE, + PRIMARY KEY(versionId, name) +) WITHOUT ROWID; + +insertPermission: +INSERT OR IGNORE INTO permission(name, maxSdkVersion, versionId) +VALUES (?, ?, ?); diff --git a/app/src/main/sqldelight/databases/1.db b/app/src/main/sqldelight/databases/1.db index cc223b7831e8af213e6cdc39488a87d4e5934ffb..9e98e02951bdda1778ab5ec693db7acc9668bf76 100644 GIT binary patch delta 1229 zcmeHG&r4KM6n@`(GxOg3e)HzXn;&mx-jKutvm(qdIiwD>-K`PNx3eM+or80%U$W7xP~tfodxDo z&q~FDhl)`|;iZKrUPg-rx2)?rrrJ;Uofymx4&+W1HiCz)#PAj_8Zr=uo2^K9;H3IE zn&4t{)(}m^u>=QgPhb}f#G%tz0*kOyDhY$lT0?BywhV>U$r;$$u>1frODh?clcVgY z;$gGO5^QX*y^80DcI7D|_c`O4|6Jl*-2@yCqro`*G2|QD|R2 z&$p%A8Np}azZtq96 zlu=9O(`vV~Ah^Ue!Tref%z6GdU0D6!g@4-xa(nTq*7jV`gnq$s&OWV-%Zs+xme00% zizrFrq?qQj+6Rx&?>+0acxJd2c1y)gG0OVTfdno2a12p$XfP3{S`GDx&~Xh37&N9~ z8^Ux)LoH(D_3`jQ!%jZ%Reo&dtOI^DAVlMS?BJ|rKMo-{A3&OuY65tH0DTK^3+WUN z;uSRd801*+z7fJG;HPUknq47h{>PXX_xtFj&dUhUR~@&Z(ycHj`{`jAkGW~jM>wX^ zLIm&eCr)0q2!a%+oB{nW&D)>SgON%EzP`5jd}K?(o> delta 1448 zcmeHHO=uHQ5Z*7F>~5NDvztw_>28xO)fQ|0ssBHyv?r;LJsRKy=Fh&iZGMGqnfq8CM~=*5dCEeax4>Pu?Dg9kl`7nip)^JZtidCa_T z2F@!31IjU5w*^6P_&fF3g;@(4Y+=rkaSwMPFkbo)42=8UcvmR3a)Y$av(eoqo^YMR z72$&4Dl}J#`^8GiN8unqLs7h~R%O$+F}SiX*}grsHJ8k8OLzQGmCV6t)GwQ+iNV$7 zfbHp??%uLc>9~bgV!qkO##~YO>9CEfsGud)SjyhHP4-As*?VaNJ&&P*Dr2aFmlnma znBDh8*o6CHT#?OK41;$;No6|s-BE5qWV^g|a2L;bPXlhY+_yAQDw0wv(f(GKY#Ig* zsO8+rY?d#vSnxHW;(!O`iX2K7AE+(xWmCBw>E7OSrl*&Uc|zJx`_|6x`3p+Uq8j+9 z4K+T!;XtePTY}$pPEh;(H5JzHccQk?Z2xc0UuX`g0eq^L9}BwBB~7@Oe?dnWGTj|R;POAy@Q ne>`g3*}k1IIvVH0%9*U_Q$B#87U4`Jh8%6kamtb#zgXV^&X|>o From f3bfe2b45e3e08635834bc3fab836c935f616b45 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Fri, 17 Jul 2026 15:18:42 +0530 Subject: [PATCH 06/19] refactor: Drop src* rows --- .../looker/droidify/data/local/sql/Version.sq | 3 --- app/src/main/sqldelight/databases/1.db | Bin 135168 -> 135168 bytes 2 files changed, 3 deletions(-) diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq index 189264f30..f60b6ad15 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq @@ -16,9 +16,6 @@ CREATE TABLE version ( apkName TEXT NOT NULL, apkSha256 BLOB NOT NULL, apkSize INTEGER NOT NULL, - srcName TEXT, - srcSha256 BLOB, - srcSize INTEGER, appId INTEGER NOT NULL REFERENCES app(id) ON DELETE CASCADE, id INTEGER NOT NULL PRIMARY KEY ); diff --git a/app/src/main/sqldelight/databases/1.db b/app/src/main/sqldelight/databases/1.db index 9e98e02951bdda1778ab5ec693db7acc9668bf76..73f0805c00a7357d51871e1cbddcd73bbc641f90 100644 GIT binary patch delta 297 zcmZozz|pXPV?w^PF#m7Pb=<`q7ua{Q?PtBq@|yWAvnG=Kk$$|o#H#0Hr;^Aax zWEYo~W^7O0e3W+{<8(d=M#sq;cw{#J=4bz4x{iT|S(AbH8!r#{Hta?*m~*dVKn2_D zBp5$2ZGOjejthtBiklP|1+>}EG6*rGF>se~)p1sE%wxaG`hk4{%OB>y%r(qPOe>h8 znK&4e7;Z77@pn#~Xf>Thp7GE23<1V$HYT2|=~u-VUoi@8KOoMynVC_3dx3#0wv4-YIGq^T z#igYgJEAx7&0{nzE=u-G%uQ7Yag7Mk;Zgtsph$2=qLHbYf|HNG6GAMrDpkSLFT~Z| zHE24c7Nh&-`~1ouREimRnBFn)?&i73U5(4?$$|oRsB8E1`}~ZGI=pO`8H5-z7`Uo9 zTR7_2m$2Pt`NlSlg^8J&sfkI0aUEkKBOgNse+54aU+=_;HrtaF8ME1#xC^GAS7&_1 pD7<}_2IFRCM%nFoI*gLc(>wJU%crL)F#cJj5U|KVV37h&1OOV(XZ`>H From e79bca802ea17e4d237edb8908a87f73ae75bbf4 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Fri, 17 Jul 2026 15:24:06 +0530 Subject: [PATCH 07/19] refactor: Normalize version table for feature and native_code --- .../looker/droidify/data/local/sql/Version.sq | 24 +++++++++++++++--- app/src/main/sqldelight/databases/1.db | Bin 135168 -> 143360 bytes 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq index f60b6ad15..eb436f1e1 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq @@ -1,7 +1,5 @@ import com.looker.droidify.sync.v2.model.LocalizedString; import kotlin.Int; -import kotlin.String; -import kotlin.collections.List; CREATE TABLE version ( added INTEGER NOT NULL, @@ -11,8 +9,6 @@ CREATE TABLE version ( maxSdkVersion INTEGER AS Int, minSdkVersion INTEGER AS Int NOT NULL, targetSdkVersion INTEGER AS Int NOT NULL, - features TEXT AS List NOT NULL, - nativeCode TEXT AS List NOT NULL, apkName TEXT NOT NULL, apkSha256 BLOB NOT NULL, apkSize INTEGER NOT NULL, @@ -30,6 +26,26 @@ CREATE TABLE permission ( PRIMARY KEY(versionId, name) ) WITHOUT ROWID; +CREATE TABLE feature ( + name TEXT NOT NULL, + versionId INTEGER NOT NULL REFERENCES version(id) ON DELETE CASCADE, + PRIMARY KEY(versionId, name) +) WITHOUT ROWID; + +CREATE TABLE native_code ( + abi TEXT NOT NULL, + versionId INTEGER NOT NULL REFERENCES version(id) ON DELETE CASCADE, + PRIMARY KEY(versionId, abi) +) WITHOUT ROWID; + insertPermission: INSERT OR IGNORE INTO permission(name, maxSdkVersion, versionId) VALUES (?, ?, ?); + +insertFeature: +INSERT OR IGNORE INTO feature(name, versionId) +VALUES (?, ?); + +insertNativeCode: +INSERT OR IGNORE INTO native_code(abi, versionId) +VALUES (?, ?); diff --git a/app/src/main/sqldelight/databases/1.db b/app/src/main/sqldelight/databases/1.db index 73f0805c00a7357d51871e1cbddcd73bbc641f90..8385425d535a741740bc8d81a05e141039eb9cf3 100644 GIT binary patch delta 1442 zcmeHHT}YEr82+BQ?c2WX`@Wg0Z*4Ze{is{&wtf+UXqmP&v6d12NG+RXKW3$Y6zPn9 zAgsoT-W$R4CJ6d5p`wB!yfg?R5-IAUi>Pj*tB|ncD#(kv>86Vg=l6Nec@O7#IB(A- zOV1n2c~8Dq5QG44KZ`e8zM#S@MEWL79D+JAMHHwLwwux>OH$Ir4s(;K*_g>YXUO7- z@Ip#vvvuywAyw2g4aciHQrq{Y_qMj}Ih5-hbhZpHjhVP2Qd>DRsC2R%S0U593RFU& z-4z&touUye=3q+%_h2Jm6onjI8buwuFDZ1Y1a8)DUMowY7LTL1D|ZWXh;H`I?1qhf z6=$_t%nd0)T12D58YYD{RF>5~{vQArNk%hl)stLX8Orlm$ zfD>{qa;m+ZSM%_wEsmA-Mo|j{(3{FpZK;md!|AQf`*);&nVPDS)$zt^y)nLgb+!Hr zp@$@$m(=!FwoG!eEtYKwJ`YP_eZ#88M8o<v_`{{Zz6J>+__9z|xSOGo{vfSa!RQHD9B`rx5Ye$2;g3I|E9ibb&Kk6A}Cbn0{_iV_5-1aZ@76blUpi)fdQ2>jo%A)O26rB^!U s(NL5t6ZT;VS1|2E0yABK|gRs??Vj7lNy9ecbopo_i1P-pje; zZ{_hBdD3I|avbMp_s{X>TiQcjE_`uSWDu;Y<$%MwYABl0@`Ch8|5$u2eBwXz&AN8% zG8Kv4R^qd13r#hl8df^dge8|Esj7-Ido%IA!Ng$y(6RLTW66JChM6qD1%<5OHcV6t zp@Yl_PC69A9U(hxl&G%-+sJj%LW+V+micQiP+Kd!Fp^o}31pHKV=$0ey_?L7(~!s! zeT<>A`bFr;NvVM$<=6vh8Ui^a2O*ZmOaTgvtDlIn2aoU&eIJe6W5 zlv2jsAp4aS<5>xk>ha)wq7=1QASV;?%;-oWP5k;w!%#faeDZ_=djlmEq!@pZbJx?Y`(K5mIiS})}DQQb%lRl*2Eqf>rV z)5Bqe;H9}Rw!pUmJr83iJk%J$0o2l-2zpuG-3Xg|4PA(!P1oh72Q}0(8go^e)8K@Q zUe*wzSN)9nMngNp9exBDbBCXKPI}yrHn___bfb!X^`nV#TyY=Ftqkw~E1BjZP=79}eX7}0Y(*Ucbl70-}29v^MkdZ6stsv&ver-k{uLO?c pedGny&F)>7ZnTzOYDJKi!fXI9Td@^N39&U_L1Ut~6|}Yu`wg50CY%5O From 469ac810a8a5cb2c6582876c02d7a25101781d88 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Fri, 17 Jul 2026 15:45:27 +0530 Subject: [PATCH 08/19] feat: Add simple insert queries for most tables These are to the best of my abilities --- .../droidify/data/local/sql/AntiFeature.sq | 12 ++++++++++ .../com/looker/droidify/data/local/sql/App.sq | 12 ++++++++++ .../droidify/data/local/sql/AppMetadata.sq | 24 +++++++++++++++++++ .../droidify/data/local/sql/Category.sq | 12 ++++++++++ .../droidify/data/local/sql/Repository.sq | 19 +++++++++++++++ .../looker/droidify/data/local/sql/Version.sq | 8 +++++++ 6 files changed, 87 insertions(+) diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AntiFeature.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AntiFeature.sq index 0105aa49c..a4bf19cfb 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AntiFeature.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AntiFeature.sq @@ -21,3 +21,15 @@ CREATE TABLE anti_feature_repo_relation ( tag TEXT NOT NULL, PRIMARY KEY(repoId, tag) ) WITHOUT ROWID; + +insertAntiFeature: +INSERT OR REPLACE INTO anti_feature(icon, name, description, locale, tag) +VALUES (?, ?, ?, ?, ?); + +insertAntiFeatureAppRelation: +INSERT OR IGNORE INTO anti_features_app_relation(tag, reason, versionId) +VALUES (?, ?, ?); + +insertAntiFeatureRepoRelation: +INSERT OR IGNORE INTO anti_feature_repo_relation(repoId, tag) +VALUES (?, ?); diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq index 91a10eeeb..76a5f8e64 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq @@ -29,3 +29,15 @@ CREATE TABLE localized_app ( OR description IS NOT NULL ) ); + +insertApp: +INSERT OR IGNORE INTO app(added, lastUpdated, preferredSigner, packageName, authorId, repoId) +VALUES (?, ?, ?, ?, ?, ?); + +selectAppId: +SELECT id FROM app +WHERE packageName = ? AND repoId = ?; + +insertLocalizedApp: +INSERT OR REPLACE INTO localized_app(appId, locale, name, summary, iconName, iconSha256, iconSize, description) +VALUES (?, ?, ?, ?, ?, ?, ?, ?); diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq index 8ba5776bf..63acd2368 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq @@ -41,3 +41,27 @@ CREATE TABLE donate ( appId INTEGER NOT NULL REFERENCES app(id) ON DELETE CASCADE, PRIMARY KEY(appId, type, value) ) WITHOUT ROWID; + +insertAuthor: +INSERT OR IGNORE INTO author(email, name, website) +VALUES (?, ?, ?); + +selectAuthorId: +SELECT id FROM author +WHERE email = ? AND name = ? AND website = ?; + +insertLinks: +INSERT OR REPLACE INTO links(license, changelog, issueTracker, translation, sourceCode, webSite, appId) +VALUES (?, ?, ?, ?, ?, ?, ?); + +insertGraphic: +INSERT OR REPLACE INTO graphic(url, type, locale, appId) +VALUES (?, ?, ?, ?); + +insertScreenshot: +INSERT OR IGNORE INTO screenshot(path, type, locale, appId) +VALUES (?, ?, ?, ?); + +insertDonate: +INSERT OR IGNORE INTO donate(type, value, appId) +VALUES (?, ?, ?); diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Category.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Category.sq index 5c005b57c..b5fb569b5 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Category.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Category.sq @@ -20,3 +20,15 @@ CREATE TABLE category_repo_relation ( defaultName TEXT NOT NULL, PRIMARY KEY(repoId, defaultName) ) WITHOUT ROWID; + +insertCategory: +INSERT OR REPLACE INTO category(icon, name, description, locale, defaultName) +VALUES (?, ?, ?, ?, ?); + +insertCategoryAppRelation: +INSERT OR IGNORE INTO category_app_relation(appId, defaultName) +VALUES (?, ?); + +insertCategoryRepoRelation: +INSERT OR IGNORE INTO category_repo_relation(repoId, defaultName) +VALUES (?, ?); diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq index cbdaf9e6b..13bc181b0 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq @@ -44,3 +44,22 @@ CREATE TABLE authentication ( initializationVector BLOB NOT NULL, repoId INTEGER NOT NULL PRIMARY KEY REFERENCES repository(id) ON DELETE CASCADE ); + +insertRepo: +INSERT INTO repository(address, webBaseUrl, fingerprint, timestamp) +VALUES (?, ?, ?, ?); + +lastInsertRowId: +SELECT last_insert_rowid(); + +insertLocalizedRepo: +INSERT OR REPLACE INTO localized_repo(repoId, locale, name, description, iconName, iconSha256, iconSize) +VALUES (?, ?, ?, ?, ?, ?, ?); + +insertMirror: +INSERT OR REPLACE INTO mirror(url, countryCode, isPrimary, repoId) +VALUES (?, ?, ?, ?); + +insertAuthentication: +INSERT OR REPLACE INTO authentication(password, username, initializationVector, repoId) +VALUES (?, ?, ?, ?); diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq index eb436f1e1..2c6a23b05 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq @@ -38,6 +38,14 @@ CREATE TABLE native_code ( PRIMARY KEY(versionId, abi) ) WITHOUT ROWID; +insertVersion: +INSERT OR IGNORE INTO version(added, whatsNew, versionName, versionCode, maxSdkVersion, minSdkVersion, targetSdkVersion, apkName, apkSha256, apkSize, appId) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + +selectVersionId: +SELECT id FROM version +WHERE appId = ? AND apkSha256 = ?; + insertPermission: INSERT OR IGNORE INTO permission(name, maxSdkVersion, versionId) VALUES (?, ?, ?); From 39dbf19010fea19eae322d960c951c4c6e228b41 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 18 Jul 2026 16:27:22 +0530 Subject: [PATCH 09/19] fix: Fix db schema outdated Signed-off-by: LooKeR --- app/src/main/sqldelight/databases/1.db | Bin 143360 -> 143360 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/src/main/sqldelight/databases/1.db b/app/src/main/sqldelight/databases/1.db index 8385425d535a741740bc8d81a05e141039eb9cf3..dc07c606cf0e47e408714e299ee4c12df516acac 100644 GIT binary patch delta 1059 zcmeHG%}Z2K6o0?>=6%eYc{6Wjw5Rj=Ugigw&J+X^Ek>gc%fymMgvbdLG#e+iEOOzD z)W(f=qf3y+Oav8$)DW&Ji56|F0jZ#cxsV{JpiP?=b)5`aq<=wh?>(H~J?GrZ@0{P* zvNE=;Ob+^;2OKHYuXye6q$aV$c13v3uV8^&RK3Kb?-SA^Zqg&hp_8msPUgj%aFH%4 z4mb1PKqDja7KW_eJ(j11^dTq7J2o@?t>b>sAsc}JGRKD#C;Pj5x)Z}^2mcr+nQ`8O zN}4qj4$`N+4BCsqm(LV~&vg$D^$sN0j5^mE#v=&hO)@#ER6)s)DmMUyl&cL9JI8Sf z+gzqyk`12^m$YJuL@LFWoKLBZ;E#!PvI=F8=-n#J!$$k6VM3%6)mVW*Q#J52aH0n9 z%Xrx^42_rw}ktO3pc8}RQ2+^N^3ubhv|a*hdg%08<+ zk)KK5MUV8*781hzH9m;f80CI&S$C)FqicY1ImCB_sLw(rJrYF;ZL!!01nFrDF+}K$ zg;xG>fX+r48m5IPV*EirZD9^VtFA#aCN`m*UN=$8@>qhOelW3JNQ-4OQ;%(Q+CqT7 zs>jZ`FBS>{a`b5>3j9ScHEYp;Qj)cm(qt_j*wOOk*sqNlc?q6#EEu zrPOkTiwhAViipM1fNoq=sh|t7U8v|nyHFJDqJKbiGObGmbt{~EzI)F7xChQT-^8jk zu_|53`Rwy@pA?f-F>mb`5|&ARhMUC;@8IWm+U9yOht z4mjV#N%y*H;9|XD+~FtBIoOj1w8F_=Hldoi8*v{BTWrK`I5tvB6K;jLNja8lJWeRTyhgEbl!3o96csz;OTkcSpX*a;%Sq7Hklh8%A_}eD-BrZAQ+`9l%kd` z>iU9?fd)B76n=&{b;ki^;`g0bf6;kkD&7C5^7m6&fbI|;kt)(j>_PdcEg_u}Q`XDE z68}LM=e}YM!|)JBI$d)v(K+t4JFd=l__!`VTZtfq5L=I68+JVVhHS9zU`=&vMzsxxFFjR{b-U!y~vFtxmA*x<43N~|b1~X|x0vo=XUa~L3 zL%S6XRWz-947kl5E3}!^R4$%9Ih5E04^2DX)%=q@=PZO^j6H6|u0K?0ie;Ly#t9*I zEsE`kY&7Ci6nnVZAnR|zL1^rD3;MXj0d^q*4dHU Date: Sat, 18 Jul 2026 16:28:24 +0530 Subject: [PATCH 10/19] refactor: Remove Room DB, repo and UI bindings Signed-off-by: LooKeR --- app/build.gradle.kts | 9 - .../1.json | 1798 ----------------- .../2.json | 1701 ---------------- .../3.json | 1530 -------------- .../com/looker/droidify/dao/IndexDaoTest.kt | 67 - .../droidify/compose/MainComposeActivity.kt | 17 +- .../compose/appDetail/AppDetailViewModel.kt | 30 +- .../compose/appList/AppListViewModel.kt | 32 +- .../compose/repoDetail/RepoDetailViewModel.kt | 20 +- .../compose/repoEdit/RepoEditViewModel.kt | 34 +- .../compose/repoList/RepoListViewModel.kt | 22 +- .../com/looker/droidify/data/AppRepository.kt | 71 - .../droidify/data/InstalledRepository.kt | 42 - .../looker/droidify/data/RepoRepository.kt | 239 --- .../droidify/data/local/DroidifyDatabase.kt | 100 - .../data/local/converters/Converters.kt | 34 - .../local/converters/PermissionConverter.kt | 21 - .../looker/droidify/data/local/dao/AppDao.kt | 281 --- .../looker/droidify/data/local/dao/AuthDao.kt | 16 - .../droidify/data/local/dao/IndexDao.kt | 363 ---- .../droidify/data/local/dao/InstalledDao.kt | 78 - .../droidify/data/local/dao/LogQueries.kt | 16 - .../looker/droidify/data/local/dao/RepoDao.kt | 68 - .../data/local/model/AntiFeatureEntity.kt | 73 - .../droidify/data/local/model/AppEntity.kt | 178 -- .../data/local/model/AuthenticationEntity.kt | 33 - .../droidify/data/local/model/AuthorEntity.kt | 33 - .../data/local/model/CategoryEntity.kt | 80 - .../droidify/data/local/model/DonateEntity.kt | 97 - .../data/local/model/GraphicEntity.kt | 85 - .../data/local/model/InstalledEntity.kt | 34 - .../droidify/data/local/model/LinksEntity.kt | 58 - .../data/local/model/LocalizedAppEntity.kt | 103 - .../data/local/model/LocalizedRepoEntity.kt | 66 - .../droidify/data/local/model/MirrorEntity.kt | 36 - .../droidify/data/local/model/RepoEntity.kt | 56 - .../data/local/model/ScreenshotEntity.kt | 118 -- .../data/local/model/VersionEntity.kt | 125 -- .../com/looker/droidify/di/DatabaseModule.kt | 46 - .../com/looker/droidify/di/RepoModule.kt | 66 - .../com/looker/droidify/work/SyncWorker.kt | 169 -- .../droidify/data/InstalledRepositoryTest.kt | 145 -- .../droidify/data/local/BaseDatabaseTest.kt | 70 - .../data/local/dao/InstalledDaoTest.kt | 193 -- .../droidify/data/local/dao/RepoDaoTest.kt | 100 - gradle/libs.versions.toml | 6 - 46 files changed, 27 insertions(+), 8532 deletions(-) delete mode 100644 app/schemas/com.looker.droidify.data.local.DroidifyDatabase/1.json delete mode 100644 app/schemas/com.looker.droidify.data.local.DroidifyDatabase/2.json delete mode 100644 app/schemas/com.looker.droidify.data.local.DroidifyDatabase/3.json delete mode 100644 app/src/androidTest/kotlin/com/looker/droidify/dao/IndexDaoTest.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/AppRepository.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/InstalledRepository.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/RepoRepository.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/DroidifyDatabase.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/converters/Converters.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/converters/PermissionConverter.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/dao/AppDao.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/dao/AuthDao.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/dao/IndexDao.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/dao/InstalledDao.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/dao/LogQueries.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/dao/RepoDao.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/AntiFeatureEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/AppEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/AuthenticationEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/AuthorEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/CategoryEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/DonateEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/GraphicEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/InstalledEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/LinksEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/LocalizedAppEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/LocalizedRepoEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/MirrorEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/RepoEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/ScreenshotEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/VersionEntity.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/di/RepoModule.kt delete mode 100644 app/src/main/kotlin/com/looker/droidify/work/SyncWorker.kt delete mode 100644 app/src/test/kotlin/com/looker/droidify/data/InstalledRepositoryTest.kt delete mode 100644 app/src/test/kotlin/com/looker/droidify/data/local/BaseDatabaseTest.kt delete mode 100644 app/src/test/kotlin/com/looker/droidify/data/local/dao/InstalledDaoTest.kt delete mode 100644 app/src/test/kotlin/com/looker/droidify/data/local/dao/RepoDaoTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 450f3308d..0b94a9f12 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -94,11 +94,6 @@ android { } } -ksp { - arg("room.schemaLocation", "$projectDir/schemas") - arg("room.generateKotlin", "true") -} - sqldelight { databases { create("DroidifyDb") { @@ -153,8 +148,6 @@ dependencies { implementation(libs.serialization) implementation(libs.okhttp) - implementation(libs.bundles.room) - ksp(libs.room.compiler) implementation(libs.bundles.sqldelight) implementation(libs.work.ktx) @@ -172,7 +165,6 @@ dependencies { testImplementation(platform(libs.junit.bom)) testImplementation(libs.bundles.test.unit) - testImplementation(libs.room.test) testImplementation(libs.robolectric) testImplementation(libs.arch.core.testing) testImplementation(libs.test.core) @@ -184,7 +176,6 @@ dependencies { testRuntimeOnly(libs.junit.vintage.engine) kspTest(libs.hilt.compiler) androidTestImplementation(libs.hilt.test) - androidTestImplementation(libs.room.test) androidTestImplementation(libs.bundles.test.android) kspAndroidTest(libs.hilt.compiler) diff --git a/app/schemas/com.looker.droidify.data.local.DroidifyDatabase/1.json b/app/schemas/com.looker.droidify.data.local.DroidifyDatabase/1.json deleted file mode 100644 index 42982e6d7..000000000 --- a/app/schemas/com.looker.droidify.data.local.DroidifyDatabase/1.json +++ /dev/null @@ -1,1798 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "28859066b021d0f5a014f372e31c0e9a", - "entities": [ - { - "tableName": "anti_feature", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`icon` TEXT, `name` TEXT NOT NULL, `description` TEXT, `locale` TEXT NOT NULL, `tag` TEXT NOT NULL, PRIMARY KEY(`tag`, `locale`))", - "fields": [ - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT" - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "tag", - "columnName": "tag", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tag", - "locale" - ] - } - }, - { - "tableName": "anti_features_app_relation", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `reason` TEXT NOT NULL, `appId` INTEGER NOT NULL, `versionCode` INTEGER NOT NULL, PRIMARY KEY(`tag`, `appId`, `versionCode`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tag", - "columnName": "tag", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "reason", - "columnName": "reason", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "versionCode", - "columnName": "versionCode", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tag", - "appId", - "versionCode" - ] - }, - "indices": [ - { - "name": "index_anti_features_app_relation_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_anti_features_app_relation_appId` ON `${TABLE_NAME}` (`appId`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "anti_feature_repo_relation", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tag` TEXT NOT NULL, PRIMARY KEY(`id`, `tag`), FOREIGN KEY(`id`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tag", - "columnName": "tag", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id", - "tag" - ] - }, - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "id" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "authentication", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`password` TEXT NOT NULL, `username` TEXT NOT NULL, `initializationVector` BLOB NOT NULL, `repoId` INTEGER NOT NULL, PRIMARY KEY(`repoId`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "password", - "columnName": "password", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "username", - "columnName": "username", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "initializationVector", - "columnName": "initializationVector", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "repoId" - ] - }, - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "author", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT, `name` TEXT, `website` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", - "fields": [ - { - "fieldPath": "email", - "columnName": "email", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT" - }, - { - "fieldPath": "website", - "columnName": "website", - "affinity": "TEXT" - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_author_email_name_website", - "unique": true, - "columnNames": [ - "email", - "name", - "website" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_author_email_name_website` ON `${TABLE_NAME}` (`email`, `name`, `website`)" - } - ] - }, - { - "tableName": "app", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`added` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, `license` TEXT, `preferredSigner` TEXT, `packageName` TEXT NOT NULL, `authorId` INTEGER NOT NULL, `repoId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`authorId`) REFERENCES `author`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "added", - "columnName": "added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lastUpdated", - "columnName": "lastUpdated", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "license", - "columnName": "license", - "affinity": "TEXT" - }, - { - "fieldPath": "preferredSigner", - "columnName": "preferredSigner", - "affinity": "TEXT" - }, - { - "fieldPath": "packageName", - "columnName": "packageName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "authorId", - "columnName": "authorId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_app_authorId", - "unique": false, - "columnNames": [ - "authorId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_app_authorId` ON `${TABLE_NAME}` (`authorId`)" - }, - { - "name": "index_app_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_app_repoId` ON `${TABLE_NAME}` (`repoId`)" - }, - { - "name": "index_app_packageName", - "unique": false, - "columnNames": [ - "packageName" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_app_packageName` ON `${TABLE_NAME}` (`packageName`)" - }, - { - "name": "index_app_packageName_repoId", - "unique": true, - "columnNames": [ - "packageName", - "repoId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_packageName_repoId` ON `${TABLE_NAME}` (`packageName`, `repoId`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "author", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "authorId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "category", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`icon` TEXT, `name` TEXT NOT NULL, `description` TEXT, `locale` TEXT NOT NULL, `defaultName` TEXT NOT NULL, PRIMARY KEY(`defaultName`, `locale`))", - "fields": [ - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT" - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "defaultName", - "columnName": "defaultName", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "defaultName", - "locale" - ] - }, - "indices": [ - { - "name": "index_category_defaultName", - "unique": false, - "columnNames": [ - "defaultName" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_category_defaultName` ON `${TABLE_NAME}` (`defaultName`)" - } - ] - }, - { - "tableName": "category_app_relation", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `defaultName` TEXT NOT NULL, PRIMARY KEY(`id`, `defaultName`), FOREIGN KEY(`id`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "defaultName", - "columnName": "defaultName", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id", - "defaultName" - ] - }, - "indices": [ - { - "name": "index_category_app_relation_defaultName", - "unique": false, - "columnNames": [ - "defaultName" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_category_app_relation_defaultName` ON `${TABLE_NAME}` (`defaultName`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "id" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "category_repo_relation", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `defaultName` TEXT NOT NULL, PRIMARY KEY(`id`, `defaultName`), FOREIGN KEY(`id`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "defaultName", - "columnName": "defaultName", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id", - "defaultName" - ] - }, - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "id" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "donate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL, `value` TEXT NOT NULL, `appId` INTEGER NOT NULL, PRIMARY KEY(`type`, `appId`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "type", - "appId" - ] - }, - "indices": [ - { - "name": "index_donate_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_donate_appId` ON `${TABLE_NAME}` (`appId`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "graphic", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL, `url` TEXT NOT NULL, `locale` TEXT NOT NULL, `appId` INTEGER NOT NULL, PRIMARY KEY(`type`, `locale`, `appId`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "type", - "locale", - "appId" - ] - }, - "indices": [ - { - "name": "index_graphic_appId_locale", - "unique": false, - "columnNames": [ - "appId", - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_graphic_appId_locale` ON `${TABLE_NAME}` (`appId`, `locale`)" - }, - { - "name": "index_graphic_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_graphic_appId` ON `${TABLE_NAME}` (`appId`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "installed", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `signature` TEXT NOT NULL, PRIMARY KEY(`packageName`))", - "fields": [ - { - "fieldPath": "packageName", - "columnName": "packageName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "version", - "columnName": "version", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "versionCode", - "columnName": "versionCode", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "signature", - "columnName": "signature", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "packageName" - ] - } - }, - { - "tableName": "link", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`changelog` TEXT, `issueTracker` TEXT, `translation` TEXT, `sourceCode` TEXT, `webSite` TEXT, `appId` INTEGER NOT NULL, PRIMARY KEY(`appId`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "changelog", - "columnName": "changelog", - "affinity": "TEXT" - }, - { - "fieldPath": "issueTracker", - "columnName": "issueTracker", - "affinity": "TEXT" - }, - { - "fieldPath": "translation", - "columnName": "translation", - "affinity": "TEXT" - }, - { - "fieldPath": "sourceCode", - "columnName": "sourceCode", - "affinity": "TEXT" - }, - { - "fieldPath": "webSite", - "columnName": "webSite", - "affinity": "TEXT" - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId" - ] - }, - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "mirror", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `countryCode` TEXT, `isPrimary` INTEGER NOT NULL, `repoId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "countryCode", - "columnName": "countryCode", - "affinity": "TEXT" - }, - { - "fieldPath": "isPrimary", - "columnName": "isPrimary", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_mirror_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_mirror_repoId` ON `${TABLE_NAME}` (`repoId`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "repository", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `webBaseUrl` TEXT, `fingerprint` TEXT NOT NULL, `timestamp` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", - "fields": [ - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "webBaseUrl", - "columnName": "webBaseUrl", - "affinity": "TEXT" - }, - { - "fieldPath": "fingerprint", - "columnName": "fingerprint", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER" - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "screenshot", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`path` TEXT NOT NULL, `type` INTEGER NOT NULL, `locale` TEXT NOT NULL, `appId` INTEGER NOT NULL, PRIMARY KEY(`path`, `type`, `locale`, `appId`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "path", - "columnName": "path", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "path", - "type", - "locale", - "appId" - ] - }, - "indices": [ - { - "name": "index_screenshot_appId_locale", - "unique": false, - "columnNames": [ - "appId", - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_screenshot_appId_locale` ON `${TABLE_NAME}` (`appId`, `locale`)" - }, - { - "name": "index_screenshot_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_screenshot_appId` ON `${TABLE_NAME}` (`appId`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "version", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`added` INTEGER NOT NULL, `whatsNew` TEXT NOT NULL, `versionName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `maxSdkVersion` INTEGER, `minSdkVersion` INTEGER NOT NULL, `targetSdkVersion` INTEGER NOT NULL, `features` TEXT NOT NULL, `nativeCode` TEXT NOT NULL, `permissions` TEXT NOT NULL, `permissionsSdk23` TEXT NOT NULL, `appId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `apk_name` TEXT NOT NULL, `apk_sha256` TEXT NOT NULL, `apk_size` INTEGER NOT NULL, `src_name` TEXT, `src_sha256` TEXT, `src_size` INTEGER, FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "added", - "columnName": "added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "whatsNew", - "columnName": "whatsNew", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "versionName", - "columnName": "versionName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "versionCode", - "columnName": "versionCode", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "maxSdkVersion", - "columnName": "maxSdkVersion", - "affinity": "INTEGER" - }, - { - "fieldPath": "minSdkVersion", - "columnName": "minSdkVersion", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "targetSdkVersion", - "columnName": "targetSdkVersion", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "features", - "columnName": "features", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "nativeCode", - "columnName": "nativeCode", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "permissions", - "columnName": "permissions", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "permissionsSdk23", - "columnName": "permissionsSdk23", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "apk.name", - "columnName": "apk_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "apk.sha256", - "columnName": "apk_sha256", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "apk.size", - "columnName": "apk_size", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "src.name", - "columnName": "src_name", - "affinity": "TEXT" - }, - { - "fieldPath": "src.sha256", - "columnName": "src_sha256", - "affinity": "TEXT" - }, - { - "fieldPath": "src.size", - "columnName": "src_size", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_version_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_version_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_version_appId_versionCode", - "unique": true, - "columnNames": [ - "appId", - "versionCode" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_version_appId_versionCode` ON `${TABLE_NAME}` (`appId`, `versionCode`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "rblog", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hash` TEXT NOT NULL, `repository` TEXT NOT NULL, `apkUrl` TEXT NOT NULL, `packageName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `versionName` TEXT NOT NULL, `tag` TEXT NOT NULL, `commit` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `reproducible` INTEGER, `error` TEXT, PRIMARY KEY(`hash`, `packageName`, `timestamp`))", - "fields": [ - { - "fieldPath": "hash", - "columnName": "hash", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "repository", - "columnName": "repository", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "apkUrl", - "columnName": "apkUrl", - "affinity": "TEXT", - "notNull": true - }, - { - "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": "tag", - "columnName": "tag", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "commit", - "columnName": "commit", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "reproducible", - "columnName": "reproducible", - "affinity": "INTEGER" - }, - { - "fieldPath": "error", - "columnName": "error", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "hash", - "packageName", - "timestamp" - ] - }, - "indices": [ - { - "name": "index_rblog_hash_packageName_timestamp", - "unique": true, - "columnNames": [ - "hash", - "packageName", - "timestamp" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_rblog_hash_packageName_timestamp` ON `${TABLE_NAME}` (`hash`, `packageName`, `timestamp`)" - }, - { - "name": "index_rblog_packageName_versionCode_reproducible", - "unique": false, - "columnNames": [ - "packageName", - "versionCode", - "reproducible" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_rblog_packageName_versionCode_reproducible` ON `${TABLE_NAME}` (`packageName`, `versionCode`, `reproducible`)" - }, - { - "name": "index_rblog_packageName_hash_reproducible", - "unique": false, - "columnNames": [ - "packageName", - "hash", - "reproducible" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_rblog_packageName_hash_reproducible` ON `${TABLE_NAME}` (`packageName`, `hash`, `reproducible`)" - } - ] - }, - { - "tableName": "download_stats", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `source` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `fDroid` INTEGER NOT NULL, `fDroidClassic` INTEGER NOT NULL, `neoStore` INTEGER NOT NULL, `droidify` INTEGER NOT NULL, `flicky` INTEGER NOT NULL, `unknown` INTEGER NOT NULL, PRIMARY KEY(`packageName`, `source`, `timestamp`))", - "fields": [ - { - "fieldPath": "packageName", - "columnName": "packageName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "source", - "columnName": "source", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "fDroid", - "columnName": "fDroid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "fDroidClassic", - "columnName": "fDroidClassic", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "neoStore", - "columnName": "neoStore", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "droidify", - "columnName": "droidify", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "flicky", - "columnName": "flicky", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "unknown", - "columnName": "unknown", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "packageName", - "source", - "timestamp" - ] - }, - "indices": [ - { - "name": "index_download_stats_packageName", - "unique": false, - "columnNames": [ - "packageName" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_download_stats_packageName` ON `${TABLE_NAME}` (`packageName`)" - }, - { - "name": "index_download_stats_timestamp", - "unique": false, - "columnNames": [ - "timestamp" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_download_stats_timestamp` ON `${TABLE_NAME}` (`timestamp`)" - } - ] - }, - { - "tableName": "DownloadStatsFile", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileName` TEXT NOT NULL, `lastModified` TEXT NOT NULL, `lastFetched` INTEGER NOT NULL, `fetchSuccess` INTEGER NOT NULL, `fileSize` INTEGER, `recordsCount` INTEGER, PRIMARY KEY(`fileName`))", - "fields": [ - { - "fieldPath": "fileName", - "columnName": "fileName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastModified", - "columnName": "lastModified", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastFetched", - "columnName": "lastFetched", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "fetchSuccess", - "columnName": "fetchSuccess", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "fileSize", - "columnName": "fileSize", - "affinity": "INTEGER" - }, - { - "fieldPath": "recordsCount", - "columnName": "recordsCount", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "fileName" - ] - }, - "indices": [ - { - "name": "index_DownloadStatsFile_fileName", - "unique": true, - "columnNames": [ - "fileName" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DownloadStatsFile_fileName` ON `${TABLE_NAME}` (`fileName`)" - }, - { - "name": "index_DownloadStatsFile_fileName_lastModified", - "unique": false, - "columnNames": [ - "fileName", - "lastModified" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadStatsFile_fileName_lastModified` ON `${TABLE_NAME}` (`fileName`, `lastModified`)" - } - ] - }, - { - "tableName": "localized_app_name", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`appId`, `locale`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_app_name_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_name_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_localized_app_name_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_name_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_app_summary", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `summary` TEXT NOT NULL, PRIMARY KEY(`appId`, `locale`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "summary", - "columnName": "summary", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_app_summary_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_summary_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_localized_app_summary_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_summary_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_app_description", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`appId`, `locale`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_app_description_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_description_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_localized_app_description_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_description_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_app_icon", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `icon_name` TEXT NOT NULL, `icon_sha256` TEXT, `icon_size` INTEGER, PRIMARY KEY(`appId`, `locale`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "icon.name", - "columnName": "icon_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "icon.sha256", - "columnName": "icon_sha256", - "affinity": "TEXT" - }, - { - "fieldPath": "icon.size", - "columnName": "icon_size", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_app_icon_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_icon_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_localized_app_icon_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_icon_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_repo_name", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`repoId`, `locale`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "repoId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_repo_name_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_name_repoId` ON `${TABLE_NAME}` (`repoId`)" - }, - { - "name": "index_localized_repo_name_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_name_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_repo_description", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `locale`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "repoId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_repo_description_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_description_repoId` ON `${TABLE_NAME}` (`repoId`)" - }, - { - "name": "index_localized_repo_description_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_description_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_repo_icon", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `icon_name` TEXT NOT NULL, `icon_sha256` TEXT, `icon_size` INTEGER, PRIMARY KEY(`repoId`, `locale`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "icon.name", - "columnName": "icon_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "icon.sha256", - "columnName": "icon_sha256", - "affinity": "TEXT" - }, - { - "fieldPath": "icon.size", - "columnName": "icon_size", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "repoId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_repo_icon_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_icon_repoId` ON `${TABLE_NAME}` (`repoId`)" - }, - { - "name": "index_localized_repo_icon_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_icon_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - } - ], - "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, '28859066b021d0f5a014f372e31c0e9a')" - ] - } -} \ No newline at end of file diff --git a/app/schemas/com.looker.droidify.data.local.DroidifyDatabase/2.json b/app/schemas/com.looker.droidify.data.local.DroidifyDatabase/2.json deleted file mode 100644 index 5944978a8..000000000 --- a/app/schemas/com.looker.droidify.data.local.DroidifyDatabase/2.json +++ /dev/null @@ -1,1701 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 2, - "identityHash": "33dd863d4cd9328fbb22b435fbf8c585", - "entities": [ - { - "tableName": "anti_feature", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`icon` TEXT, `name` TEXT NOT NULL, `description` TEXT, `locale` TEXT NOT NULL, `tag` TEXT NOT NULL, PRIMARY KEY(`tag`, `locale`))", - "fields": [ - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT" - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "tag", - "columnName": "tag", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tag", - "locale" - ] - } - }, - { - "tableName": "anti_features_app_relation", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `reason` TEXT NOT NULL, `appId` INTEGER NOT NULL, `versionCode` INTEGER NOT NULL, PRIMARY KEY(`tag`, `appId`, `versionCode`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tag", - "columnName": "tag", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "reason", - "columnName": "reason", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "versionCode", - "columnName": "versionCode", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tag", - "appId", - "versionCode" - ] - }, - "indices": [ - { - "name": "index_anti_features_app_relation_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_anti_features_app_relation_appId` ON `${TABLE_NAME}` (`appId`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "anti_feature_repo_relation", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tag` TEXT NOT NULL, PRIMARY KEY(`id`, `tag`), FOREIGN KEY(`id`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tag", - "columnName": "tag", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id", - "tag" - ] - }, - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "id" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "authentication", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`password` TEXT NOT NULL, `username` TEXT NOT NULL, `initializationVector` BLOB NOT NULL, `repoId` INTEGER NOT NULL, PRIMARY KEY(`repoId`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "password", - "columnName": "password", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "username", - "columnName": "username", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "initializationVector", - "columnName": "initializationVector", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "repoId" - ] - }, - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "author", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT, `name` TEXT, `website` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", - "fields": [ - { - "fieldPath": "email", - "columnName": "email", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT" - }, - { - "fieldPath": "website", - "columnName": "website", - "affinity": "TEXT" - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_author_email_name_website", - "unique": true, - "columnNames": [ - "email", - "name", - "website" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_author_email_name_website` ON `${TABLE_NAME}` (`email`, `name`, `website`)" - } - ] - }, - { - "tableName": "app", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`added` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, `license` TEXT, `preferredSigner` TEXT, `packageName` TEXT NOT NULL, `authorId` INTEGER NOT NULL, `repoId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`authorId`) REFERENCES `author`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "added", - "columnName": "added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lastUpdated", - "columnName": "lastUpdated", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "license", - "columnName": "license", - "affinity": "TEXT" - }, - { - "fieldPath": "preferredSigner", - "columnName": "preferredSigner", - "affinity": "TEXT" - }, - { - "fieldPath": "packageName", - "columnName": "packageName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "authorId", - "columnName": "authorId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_app_authorId", - "unique": false, - "columnNames": [ - "authorId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_app_authorId` ON `${TABLE_NAME}` (`authorId`)" - }, - { - "name": "index_app_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_app_repoId` ON `${TABLE_NAME}` (`repoId`)" - }, - { - "name": "index_app_packageName", - "unique": false, - "columnNames": [ - "packageName" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_app_packageName` ON `${TABLE_NAME}` (`packageName`)" - }, - { - "name": "index_app_packageName_repoId", - "unique": true, - "columnNames": [ - "packageName", - "repoId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_packageName_repoId` ON `${TABLE_NAME}` (`packageName`, `repoId`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "author", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "authorId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "category", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`icon` TEXT, `name` TEXT NOT NULL, `description` TEXT, `locale` TEXT NOT NULL, `defaultName` TEXT NOT NULL, PRIMARY KEY(`defaultName`, `locale`))", - "fields": [ - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT" - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "defaultName", - "columnName": "defaultName", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "defaultName", - "locale" - ] - }, - "indices": [ - { - "name": "index_category_defaultName", - "unique": false, - "columnNames": [ - "defaultName" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_category_defaultName` ON `${TABLE_NAME}` (`defaultName`)" - } - ] - }, - { - "tableName": "category_app_relation", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `defaultName` TEXT NOT NULL, PRIMARY KEY(`id`, `defaultName`), FOREIGN KEY(`id`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "defaultName", - "columnName": "defaultName", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id", - "defaultName" - ] - }, - "indices": [ - { - "name": "index_category_app_relation_defaultName", - "unique": false, - "columnNames": [ - "defaultName" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_category_app_relation_defaultName` ON `${TABLE_NAME}` (`defaultName`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "id" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "category_repo_relation", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `defaultName` TEXT NOT NULL, PRIMARY KEY(`id`, `defaultName`), FOREIGN KEY(`id`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "defaultName", - "columnName": "defaultName", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id", - "defaultName" - ] - }, - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "id" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "donate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL, `value` TEXT NOT NULL, `appId` INTEGER NOT NULL, PRIMARY KEY(`type`, `appId`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "type", - "appId" - ] - }, - "indices": [ - { - "name": "index_donate_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_donate_appId` ON `${TABLE_NAME}` (`appId`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "graphic", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL, `url` TEXT NOT NULL, `locale` TEXT NOT NULL, `appId` INTEGER NOT NULL, PRIMARY KEY(`type`, `locale`, `appId`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "type", - "locale", - "appId" - ] - }, - "indices": [ - { - "name": "index_graphic_appId_locale", - "unique": false, - "columnNames": [ - "appId", - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_graphic_appId_locale` ON `${TABLE_NAME}` (`appId`, `locale`)" - }, - { - "name": "index_graphic_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_graphic_appId` ON `${TABLE_NAME}` (`appId`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "installed", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `signature` TEXT NOT NULL, PRIMARY KEY(`packageName`))", - "fields": [ - { - "fieldPath": "packageName", - "columnName": "packageName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "version", - "columnName": "version", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "versionCode", - "columnName": "versionCode", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "signature", - "columnName": "signature", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "packageName" - ] - } - }, - { - "tableName": "link", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`changelog` TEXT, `issueTracker` TEXT, `translation` TEXT, `sourceCode` TEXT, `webSite` TEXT, `appId` INTEGER NOT NULL, PRIMARY KEY(`appId`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "changelog", - "columnName": "changelog", - "affinity": "TEXT" - }, - { - "fieldPath": "issueTracker", - "columnName": "issueTracker", - "affinity": "TEXT" - }, - { - "fieldPath": "translation", - "columnName": "translation", - "affinity": "TEXT" - }, - { - "fieldPath": "sourceCode", - "columnName": "sourceCode", - "affinity": "TEXT" - }, - { - "fieldPath": "webSite", - "columnName": "webSite", - "affinity": "TEXT" - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId" - ] - }, - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "mirror", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `countryCode` TEXT, `isPrimary` INTEGER NOT NULL, `repoId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "countryCode", - "columnName": "countryCode", - "affinity": "TEXT" - }, - { - "fieldPath": "isPrimary", - "columnName": "isPrimary", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_mirror_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_mirror_repoId` ON `${TABLE_NAME}` (`repoId`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "repository", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `webBaseUrl` TEXT, `fingerprint` TEXT NOT NULL, `timestamp` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", - "fields": [ - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "webBaseUrl", - "columnName": "webBaseUrl", - "affinity": "TEXT" - }, - { - "fieldPath": "fingerprint", - "columnName": "fingerprint", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER" - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "screenshot", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`path` TEXT NOT NULL, `type` INTEGER NOT NULL, `locale` TEXT NOT NULL, `appId` INTEGER NOT NULL, PRIMARY KEY(`path`, `type`, `locale`, `appId`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "path", - "columnName": "path", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "path", - "type", - "locale", - "appId" - ] - }, - "indices": [ - { - "name": "index_screenshot_appId_locale", - "unique": false, - "columnNames": [ - "appId", - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_screenshot_appId_locale` ON `${TABLE_NAME}` (`appId`, `locale`)" - }, - { - "name": "index_screenshot_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_screenshot_appId` ON `${TABLE_NAME}` (`appId`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "version", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`added` INTEGER NOT NULL, `whatsNew` TEXT NOT NULL, `versionName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `maxSdkVersion` INTEGER, `minSdkVersion` INTEGER NOT NULL, `targetSdkVersion` INTEGER NOT NULL, `features` TEXT NOT NULL, `nativeCode` TEXT NOT NULL, `permissions` TEXT NOT NULL, `permissionsSdk23` TEXT NOT NULL, `appId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `apk_name` TEXT NOT NULL, `apk_sha256` TEXT NOT NULL, `apk_size` INTEGER NOT NULL, `src_name` TEXT, `src_sha256` TEXT, `src_size` INTEGER, FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "added", - "columnName": "added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "whatsNew", - "columnName": "whatsNew", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "versionName", - "columnName": "versionName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "versionCode", - "columnName": "versionCode", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "maxSdkVersion", - "columnName": "maxSdkVersion", - "affinity": "INTEGER" - }, - { - "fieldPath": "minSdkVersion", - "columnName": "minSdkVersion", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "targetSdkVersion", - "columnName": "targetSdkVersion", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "features", - "columnName": "features", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "nativeCode", - "columnName": "nativeCode", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "permissions", - "columnName": "permissions", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "permissionsSdk23", - "columnName": "permissionsSdk23", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "apk.name", - "columnName": "apk_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "apk.sha256", - "columnName": "apk_sha256", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "apk.size", - "columnName": "apk_size", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "src.name", - "columnName": "src_name", - "affinity": "TEXT" - }, - { - "fieldPath": "src.sha256", - "columnName": "src_sha256", - "affinity": "TEXT" - }, - { - "fieldPath": "src.size", - "columnName": "src_size", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_version_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_version_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_version_appId_versionCode", - "unique": true, - "columnNames": [ - "appId", - "versionCode" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_version_appId_versionCode` ON `${TABLE_NAME}` (`appId`, `versionCode`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "rblog", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`hash` TEXT NOT NULL, `repository` TEXT NOT NULL, `apkUrl` TEXT NOT NULL, `packageName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `versionName` TEXT NOT NULL, `tag` TEXT NOT NULL, `commit` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `reproducible` INTEGER, `error` TEXT, PRIMARY KEY(`hash`, `packageName`, `timestamp`))", - "fields": [ - { - "fieldPath": "hash", - "columnName": "hash", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "repository", - "columnName": "repository", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "apkUrl", - "columnName": "apkUrl", - "affinity": "TEXT", - "notNull": true - }, - { - "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": "tag", - "columnName": "tag", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "commit", - "columnName": "commit", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "reproducible", - "columnName": "reproducible", - "affinity": "INTEGER" - }, - { - "fieldPath": "error", - "columnName": "error", - "affinity": "TEXT" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "hash", - "packageName", - "timestamp" - ] - }, - "indices": [ - { - "name": "index_rblog_hash_packageName_timestamp", - "unique": true, - "columnNames": [ - "hash", - "packageName", - "timestamp" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_rblog_hash_packageName_timestamp` ON `${TABLE_NAME}` (`hash`, `packageName`, `timestamp`)" - }, - { - "name": "index_rblog_packageName_versionCode_reproducible", - "unique": false, - "columnNames": [ - "packageName", - "versionCode", - "reproducible" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_rblog_packageName_versionCode_reproducible` ON `${TABLE_NAME}` (`packageName`, `versionCode`, `reproducible`)" - }, - { - "name": "index_rblog_packageName_hash_reproducible", - "unique": false, - "columnNames": [ - "packageName", - "hash", - "reproducible" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_rblog_packageName_hash_reproducible` ON `${TABLE_NAME}` (`packageName`, `hash`, `reproducible`)" - } - ] - }, - { - "tableName": "download_stats", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `source` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `downloads` INTEGER NOT NULL, PRIMARY KEY(`packageName`, `source`, `timestamp`))", - "fields": [ - { - "fieldPath": "packageName", - "columnName": "packageName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "source", - "columnName": "source", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "downloads", - "columnName": "downloads", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "packageName", - "source", - "timestamp" - ] - }, - "indices": [ - { - "name": "index_download_stats_packageName", - "unique": false, - "columnNames": [ - "packageName" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_download_stats_packageName` ON `${TABLE_NAME}` (`packageName`)" - }, - { - "name": "index_download_stats_timestamp", - "unique": false, - "columnNames": [ - "timestamp" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_download_stats_timestamp` ON `${TABLE_NAME}` (`timestamp`)" - } - ] - }, - { - "tableName": "localized_app_name", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`appId`, `locale`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_app_name_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_name_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_localized_app_name_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_name_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_app_summary", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `summary` TEXT NOT NULL, PRIMARY KEY(`appId`, `locale`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "summary", - "columnName": "summary", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_app_summary_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_summary_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_localized_app_summary_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_summary_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_app_description", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`appId`, `locale`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_app_description_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_description_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_localized_app_description_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_description_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_app_icon", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `icon_name` TEXT NOT NULL, `icon_sha256` TEXT, `icon_size` INTEGER, PRIMARY KEY(`appId`, `locale`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "icon.name", - "columnName": "icon_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "icon.sha256", - "columnName": "icon_sha256", - "affinity": "TEXT" - }, - { - "fieldPath": "icon.size", - "columnName": "icon_size", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_app_icon_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_icon_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_localized_app_icon_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_icon_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_repo_name", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`repoId`, `locale`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "repoId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_repo_name_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_name_repoId` ON `${TABLE_NAME}` (`repoId`)" - }, - { - "name": "index_localized_repo_name_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_name_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_repo_description", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `locale`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "repoId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_repo_description_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_description_repoId` ON `${TABLE_NAME}` (`repoId`)" - }, - { - "name": "index_localized_repo_description_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_description_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_repo_icon", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `icon_name` TEXT NOT NULL, `icon_sha256` TEXT, `icon_size` INTEGER, PRIMARY KEY(`repoId`, `locale`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "icon.name", - "columnName": "icon_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "icon.sha256", - "columnName": "icon_sha256", - "affinity": "TEXT" - }, - { - "fieldPath": "icon.size", - "columnName": "icon_size", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "repoId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_repo_icon_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_icon_repoId` ON `${TABLE_NAME}` (`repoId`)" - }, - { - "name": "index_localized_repo_icon_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_icon_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - } - ], - "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, '33dd863d4cd9328fbb22b435fbf8c585')" - ] - } -} \ No newline at end of file diff --git a/app/schemas/com.looker.droidify.data.local.DroidifyDatabase/3.json b/app/schemas/com.looker.droidify.data.local.DroidifyDatabase/3.json deleted file mode 100644 index 104d3b651..000000000 --- a/app/schemas/com.looker.droidify.data.local.DroidifyDatabase/3.json +++ /dev/null @@ -1,1530 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 3, - "identityHash": "8fe42dadf567e71fbe946fa54c9eb424", - "entities": [ - { - "tableName": "anti_feature", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`icon` TEXT, `name` TEXT NOT NULL, `description` TEXT, `locale` TEXT NOT NULL, `tag` TEXT NOT NULL, PRIMARY KEY(`tag`, `locale`))", - "fields": [ - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT" - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "tag", - "columnName": "tag", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tag", - "locale" - ] - } - }, - { - "tableName": "anti_features_app_relation", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag` TEXT NOT NULL, `reason` TEXT NOT NULL, `appId` INTEGER NOT NULL, `versionCode` INTEGER NOT NULL, PRIMARY KEY(`tag`, `appId`, `versionCode`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "tag", - "columnName": "tag", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "reason", - "columnName": "reason", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "versionCode", - "columnName": "versionCode", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "tag", - "appId", - "versionCode" - ] - }, - "indices": [ - { - "name": "index_anti_features_app_relation_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_anti_features_app_relation_appId` ON `${TABLE_NAME}` (`appId`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "anti_feature_repo_relation", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tag` TEXT NOT NULL, PRIMARY KEY(`id`, `tag`), FOREIGN KEY(`id`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tag", - "columnName": "tag", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id", - "tag" - ] - }, - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "id" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "authentication", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`password` TEXT NOT NULL, `username` TEXT NOT NULL, `initializationVector` BLOB NOT NULL, `repoId` INTEGER NOT NULL, PRIMARY KEY(`repoId`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "password", - "columnName": "password", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "username", - "columnName": "username", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "initializationVector", - "columnName": "initializationVector", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "repoId" - ] - }, - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "author", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT, `name` TEXT, `website` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", - "fields": [ - { - "fieldPath": "email", - "columnName": "email", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT" - }, - { - "fieldPath": "website", - "columnName": "website", - "affinity": "TEXT" - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_author_email_name_website", - "unique": true, - "columnNames": [ - "email", - "name", - "website" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_author_email_name_website` ON `${TABLE_NAME}` (`email`, `name`, `website`)" - } - ] - }, - { - "tableName": "app", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`added` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, `license` TEXT, `preferredSigner` TEXT, `packageName` TEXT NOT NULL, `authorId` INTEGER NOT NULL, `repoId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`authorId`) REFERENCES `author`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "added", - "columnName": "added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "lastUpdated", - "columnName": "lastUpdated", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "license", - "columnName": "license", - "affinity": "TEXT" - }, - { - "fieldPath": "preferredSigner", - "columnName": "preferredSigner", - "affinity": "TEXT" - }, - { - "fieldPath": "packageName", - "columnName": "packageName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "authorId", - "columnName": "authorId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_app_authorId", - "unique": false, - "columnNames": [ - "authorId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_app_authorId` ON `${TABLE_NAME}` (`authorId`)" - }, - { - "name": "index_app_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_app_repoId` ON `${TABLE_NAME}` (`repoId`)" - }, - { - "name": "index_app_packageName", - "unique": false, - "columnNames": [ - "packageName" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_app_packageName` ON `${TABLE_NAME}` (`packageName`)" - }, - { - "name": "index_app_packageName_repoId", - "unique": true, - "columnNames": [ - "packageName", - "repoId" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_app_packageName_repoId` ON `${TABLE_NAME}` (`packageName`, `repoId`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "author", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "authorId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "category", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`icon` TEXT, `name` TEXT NOT NULL, `description` TEXT, `locale` TEXT NOT NULL, `defaultName` TEXT NOT NULL, PRIMARY KEY(`defaultName`, `locale`))", - "fields": [ - { - "fieldPath": "icon", - "columnName": "icon", - "affinity": "TEXT" - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT" - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "defaultName", - "columnName": "defaultName", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "defaultName", - "locale" - ] - }, - "indices": [ - { - "name": "index_category_defaultName", - "unique": false, - "columnNames": [ - "defaultName" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_category_defaultName` ON `${TABLE_NAME}` (`defaultName`)" - } - ] - }, - { - "tableName": "category_app_relation", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `defaultName` TEXT NOT NULL, PRIMARY KEY(`id`, `defaultName`), FOREIGN KEY(`id`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "defaultName", - "columnName": "defaultName", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id", - "defaultName" - ] - }, - "indices": [ - { - "name": "index_category_app_relation_defaultName", - "unique": false, - "columnNames": [ - "defaultName" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_category_app_relation_defaultName` ON `${TABLE_NAME}` (`defaultName`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "id" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "category_repo_relation", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `defaultName` TEXT NOT NULL, PRIMARY KEY(`id`, `defaultName`), FOREIGN KEY(`id`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "defaultName", - "columnName": "defaultName", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id", - "defaultName" - ] - }, - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "id" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "donate", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL, `value` TEXT NOT NULL, `appId` INTEGER NOT NULL, PRIMARY KEY(`type`, `appId`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "value", - "columnName": "value", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "type", - "appId" - ] - }, - "indices": [ - { - "name": "index_donate_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_donate_appId` ON `${TABLE_NAME}` (`appId`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "graphic", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL, `url` TEXT NOT NULL, `locale` TEXT NOT NULL, `appId` INTEGER NOT NULL, PRIMARY KEY(`type`, `locale`, `appId`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "type", - "locale", - "appId" - ] - }, - "indices": [ - { - "name": "index_graphic_appId_locale", - "unique": false, - "columnNames": [ - "appId", - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_graphic_appId_locale` ON `${TABLE_NAME}` (`appId`, `locale`)" - }, - { - "name": "index_graphic_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_graphic_appId` ON `${TABLE_NAME}` (`appId`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "installed", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `signature` TEXT NOT NULL, PRIMARY KEY(`packageName`))", - "fields": [ - { - "fieldPath": "packageName", - "columnName": "packageName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "version", - "columnName": "version", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "versionCode", - "columnName": "versionCode", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "signature", - "columnName": "signature", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "packageName" - ] - } - }, - { - "tableName": "link", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`changelog` TEXT, `issueTracker` TEXT, `translation` TEXT, `sourceCode` TEXT, `webSite` TEXT, `appId` INTEGER NOT NULL, PRIMARY KEY(`appId`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "changelog", - "columnName": "changelog", - "affinity": "TEXT" - }, - { - "fieldPath": "issueTracker", - "columnName": "issueTracker", - "affinity": "TEXT" - }, - { - "fieldPath": "translation", - "columnName": "translation", - "affinity": "TEXT" - }, - { - "fieldPath": "sourceCode", - "columnName": "sourceCode", - "affinity": "TEXT" - }, - { - "fieldPath": "webSite", - "columnName": "webSite", - "affinity": "TEXT" - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId" - ] - }, - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "mirror", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `countryCode` TEXT, `isPrimary` INTEGER NOT NULL, `repoId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "url", - "columnName": "url", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "countryCode", - "columnName": "countryCode", - "affinity": "TEXT" - }, - { - "fieldPath": "isPrimary", - "columnName": "isPrimary", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_mirror_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_mirror_repoId` ON `${TABLE_NAME}` (`repoId`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "repository", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `webBaseUrl` TEXT, `fingerprint` TEXT NOT NULL, `timestamp` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", - "fields": [ - { - "fieldPath": "address", - "columnName": "address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "webBaseUrl", - "columnName": "webBaseUrl", - "affinity": "TEXT" - }, - { - "fieldPath": "fingerprint", - "columnName": "fingerprint", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER" - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - } - }, - { - "tableName": "screenshot", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`path` TEXT NOT NULL, `type` INTEGER NOT NULL, `locale` TEXT NOT NULL, `appId` INTEGER NOT NULL, PRIMARY KEY(`path`, `type`, `locale`, `appId`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "path", - "columnName": "path", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "path", - "type", - "locale", - "appId" - ] - }, - "indices": [ - { - "name": "index_screenshot_appId_locale", - "unique": false, - "columnNames": [ - "appId", - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_screenshot_appId_locale` ON `${TABLE_NAME}` (`appId`, `locale`)" - }, - { - "name": "index_screenshot_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_screenshot_appId` ON `${TABLE_NAME}` (`appId`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "version", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`added` INTEGER NOT NULL, `whatsNew` TEXT NOT NULL, `versionName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `maxSdkVersion` INTEGER, `minSdkVersion` INTEGER NOT NULL, `targetSdkVersion` INTEGER NOT NULL, `features` TEXT NOT NULL, `nativeCode` TEXT NOT NULL, `permissions` TEXT NOT NULL, `permissionsSdk23` TEXT NOT NULL, `appId` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `apk_name` TEXT NOT NULL, `apk_sha256` TEXT NOT NULL, `apk_size` INTEGER NOT NULL, `src_name` TEXT, `src_sha256` TEXT, `src_size` INTEGER, FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "added", - "columnName": "added", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "whatsNew", - "columnName": "whatsNew", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "versionName", - "columnName": "versionName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "versionCode", - "columnName": "versionCode", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "maxSdkVersion", - "columnName": "maxSdkVersion", - "affinity": "INTEGER" - }, - { - "fieldPath": "minSdkVersion", - "columnName": "minSdkVersion", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "targetSdkVersion", - "columnName": "targetSdkVersion", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "features", - "columnName": "features", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "nativeCode", - "columnName": "nativeCode", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "permissions", - "columnName": "permissions", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "permissionsSdk23", - "columnName": "permissionsSdk23", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "apk.name", - "columnName": "apk_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "apk.sha256", - "columnName": "apk_sha256", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "apk.size", - "columnName": "apk_size", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "src.name", - "columnName": "src_name", - "affinity": "TEXT" - }, - { - "fieldPath": "src.sha256", - "columnName": "src_sha256", - "affinity": "TEXT" - }, - { - "fieldPath": "src.size", - "columnName": "src_size", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_version_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_version_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_version_appId_versionCode", - "unique": true, - "columnNames": [ - "appId", - "versionCode" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_version_appId_versionCode` ON `${TABLE_NAME}` (`appId`, `versionCode`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_app_name", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`appId`, `locale`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_app_name_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_name_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_localized_app_name_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_name_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_app_summary", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `summary` TEXT NOT NULL, PRIMARY KEY(`appId`, `locale`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "summary", - "columnName": "summary", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_app_summary_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_summary_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_localized_app_summary_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_summary_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_app_description", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`appId`, `locale`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_app_description_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_description_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_localized_app_description_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_description_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_app_icon", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`appId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `icon_name` TEXT NOT NULL, `icon_sha256` TEXT, `icon_size` INTEGER, PRIMARY KEY(`appId`, `locale`), FOREIGN KEY(`appId`) REFERENCES `app`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "appId", - "columnName": "appId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "icon.name", - "columnName": "icon_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "icon.sha256", - "columnName": "icon_sha256", - "affinity": "TEXT" - }, - { - "fieldPath": "icon.size", - "columnName": "icon_size", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "appId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_app_icon_appId", - "unique": false, - "columnNames": [ - "appId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_icon_appId` ON `${TABLE_NAME}` (`appId`)" - }, - { - "name": "index_localized_app_icon_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_app_icon_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "app", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "appId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_repo_name", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`repoId`, `locale`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "repoId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_repo_name_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_name_repoId` ON `${TABLE_NAME}` (`repoId`)" - }, - { - "name": "index_localized_repo_name_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_name_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_repo_description", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `locale`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "description", - "columnName": "description", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "repoId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_repo_description_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_description_repoId` ON `${TABLE_NAME}` (`repoId`)" - }, - { - "name": "index_localized_repo_description_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_description_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "localized_repo_icon", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `locale` TEXT NOT NULL, `icon_name` TEXT NOT NULL, `icon_sha256` TEXT, `icon_size` INTEGER, PRIMARY KEY(`repoId`, `locale`), FOREIGN KEY(`repoId`) REFERENCES `repository`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "repoId", - "columnName": "repoId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "locale", - "columnName": "locale", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "icon.name", - "columnName": "icon_name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "icon.sha256", - "columnName": "icon_sha256", - "affinity": "TEXT" - }, - { - "fieldPath": "icon.size", - "columnName": "icon_size", - "affinity": "INTEGER" - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "repoId", - "locale" - ] - }, - "indices": [ - { - "name": "index_localized_repo_icon_repoId", - "unique": false, - "columnNames": [ - "repoId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_icon_repoId` ON `${TABLE_NAME}` (`repoId`)" - }, - { - "name": "index_localized_repo_icon_locale", - "unique": false, - "columnNames": [ - "locale" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_localized_repo_icon_locale` ON `${TABLE_NAME}` (`locale`)" - } - ], - "foreignKeys": [ - { - "table": "repository", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "repoId" - ], - "referencedColumns": [ - "id" - ] - } - ] - } - ], - "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, '8fe42dadf567e71fbe946fa54c9eb424')" - ] - } -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/looker/droidify/dao/IndexDaoTest.kt b/app/src/androidTest/kotlin/com/looker/droidify/dao/IndexDaoTest.kt deleted file mode 100644 index 3e76385b8..000000000 --- a/app/src/androidTest/kotlin/com/looker/droidify/dao/IndexDaoTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.looker.droidify.dao - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.looker.droidify.data.local.DroidifyDatabase -import com.looker.droidify.data.local.dao.IndexDao -import com.looker.droidify.data.model.Fingerprint -import com.looker.droidify.sync.JsonParser -import com.looker.droidify.sync.common.Izzy -import com.looker.droidify.sync.common.assets -import com.looker.droidify.sync.common.benchmark -import com.looker.droidify.sync.v2.model.IndexV2 -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.decodeFromStream -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import javax.inject.Inject -import kotlin.system.measureTimeMillis -import kotlin.time.Duration.Companion.minutes - -@HiltAndroidTest -@RunWith(AndroidJUnit4::class) -class IndexDaoTest { - - @get:Rule - val hiltRule = HiltAndroidRule(this) - - private lateinit var dispatcher: CoroutineDispatcher - - @Inject - lateinit var database: DroidifyDatabase - - @Inject - lateinit var dao: IndexDao - - private lateinit var index: IndexV2 - private val fingerprint: Fingerprint = Izzy.fingerprint!! - - @OptIn(ExperimentalSerializationApi::class) - @Before - fun setUp() { - hiltRule.inject() - dispatcher = StandardTestDispatcher() - index = JsonParser.decodeFromStream(assets("izzy_index_v2.json")) - } - - @Test - fun benchmark_insert_full_new() = runTest(dispatcher, timeout = 5.minutes) { - val output = - benchmark(5, extraMessage = "IndexDao.insertIndex – empty DB (fresh insert)") { - database.clearAllTables() - measureTimeMillis { - dao.insertIndex( - fingerprint = fingerprint, - index = index, - ) - } - } - println(output) - } -} diff --git a/app/src/main/kotlin/com/looker/droidify/compose/MainComposeActivity.kt b/app/src/main/kotlin/com/looker/droidify/compose/MainComposeActivity.kt index 679fa501e..58d3c043d 100644 --- a/app/src/main/kotlin/com/looker/droidify/compose/MainComposeActivity.kt +++ b/app/src/main/kotlin/com/looker/droidify/compose/MainComposeActivity.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier -import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import com.looker.droidify.compose.appDetail.navigation.appDetail @@ -25,32 +24,18 @@ import com.looker.droidify.compose.repoList.navigation.repoList import com.looker.droidify.compose.settings.navigation.navigateToSettings import com.looker.droidify.compose.settings.navigation.settings import com.looker.droidify.compose.theme.DroidifyTheme -import com.looker.droidify.data.RepoRepository -import com.looker.droidify.model.Repository import com.looker.droidify.utility.common.requestNotificationPermission import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import javax.inject.Inject @AndroidEntryPoint class MainComposeActivity : ComponentActivity() { - @Inject - lateinit var repository: RepoRepository - private val notificationPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - lifecycleScope.launch { - if (repository.repos.first().isEmpty()) { - Repository.defaultRepositories.forEach { - repository.insertRepo(it.address, it.fingerprint, null, null, it.name, it.description) - } - } - } + // TODO(sqldelight): seed default repositories via SQLDelight-backed repository enableEdgeToEdge() requestNotificationPermission(request = notificationPermission::launch) setContent { diff --git a/app/src/main/kotlin/com/looker/droidify/compose/appDetail/AppDetailViewModel.kt b/app/src/main/kotlin/com/looker/droidify/compose/appDetail/AppDetailViewModel.kt index 035c7daa8..a3a99d300 100644 --- a/app/src/main/kotlin/com/looker/droidify/compose/appDetail/AppDetailViewModel.kt +++ b/app/src/main/kotlin/com/looker/droidify/compose/appDetail/AppDetailViewModel.kt @@ -2,25 +2,19 @@ package com.looker.droidify.compose.appDetail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import com.looker.droidify.data.AppRepository -import com.looker.droidify.data.RepoRepository import com.looker.droidify.data.model.App import com.looker.droidify.data.model.Package -import com.looker.droidify.data.model.PackageName import com.looker.droidify.data.model.Repo import com.looker.droidify.datastore.CustomButtonRepository import com.looker.droidify.datastore.model.CustomButton import com.looker.droidify.utility.common.extension.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart import javax.inject.Inject @HiltViewModel class AppDetailViewModel @Inject constructor( - private val appRepository: AppRepository, - private val repoRepository: RepoRepository, private val customButtonRepository: CustomButtonRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -32,26 +26,8 @@ class AppDetailViewModel @Inject constructor( val customButtons: StateFlow> = customButtonRepository.buttons .asStateFlow(emptyList()) - val state: StateFlow = appRepository - .getApp(PackageName(packageName)) - .map { apps -> - when { - apps.isEmpty() -> AppDetailState.Error("No app found for $packageName") - else -> AppDetailState.Success( - app = apps.first(), - packages = apps.flatMap { - val repo = repoRepository.getRepo(it.repoId.toInt()) - if (repo != null && it.packages != null) { - it.packages.map { pkg -> pkg to repo } - } else { - emptyList() - } - }.sortedByDescending { (pkg, _) -> pkg.manifest.versionCode }, - ) - } - } - .onStart { emit(AppDetailState.Loading) } - .asStateFlow(AppDetailState.Loading) + // TODO(sqldelight): reimplement with SQLDelight-backed repository + val state: StateFlow = MutableStateFlow(AppDetailState.Loading) } sealed interface AppDetailState { diff --git a/app/src/main/kotlin/com/looker/droidify/compose/appList/AppListViewModel.kt b/app/src/main/kotlin/com/looker/droidify/compose/appList/AppListViewModel.kt index 99fd6d066..5adc5dfb0 100644 --- a/app/src/main/kotlin/com/looker/droidify/compose/appList/AppListViewModel.kt +++ b/app/src/main/kotlin/com/looker/droidify/compose/appList/AppListViewModel.kt @@ -1,9 +1,7 @@ package com.looker.droidify.compose.appList import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.ViewModel -import com.looker.droidify.data.AppRepository import com.looker.droidify.data.model.AppMinimal import com.looker.droidify.datastore.SettingsRepository import com.looker.droidify.datastore.get @@ -11,27 +9,19 @@ import com.looker.droidify.datastore.model.SortOrder import com.looker.droidify.sync.v2.model.DefaultName import com.looker.droidify.utility.common.extension.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChanged import javax.inject.Inject @HiltViewModel -@OptIn(FlowPreview::class) class AppListViewModel @Inject constructor( - private val appRepository: AppRepository, settingsRepository: SettingsRepository, ) : ViewModel() { val searchQuery = TextFieldState("") - private val searchQueryStream = snapshotFlow { searchQuery.text.toString() }.debounce(300) - val categories = appRepository.categories.asStateFlow(emptyList()) + // TODO(sqldelight): reimplement with SQLDelight-backed repository + val categories: StateFlow> = MutableStateFlow(emptyList()) private val _selectedCategories = MutableStateFlow>(emptySet()) val selectedCategories: StateFlow> = _selectedCategories @@ -39,25 +29,11 @@ class AppListViewModel @Inject constructor( // Favourites state private val _favouritesOnly = MutableStateFlow(false) val favouritesOnly: StateFlow = _favouritesOnly - private val favouriteApps: Flow> = settingsRepository.get { favouriteApps }.distinctUntilChanged() val sortOrderFlow = settingsRepository.get { sortOrder }.asStateFlow(SortOrder.UPDATED) - @OptIn(ExperimentalCoroutinesApi::class) - val appsState: StateFlow> = combine( - searchQueryStream, - selectedCategories, - sortOrderFlow, - favouritesOnly, - favouriteApps, - ) { searchQuery, categories, sortOrder, favOnly, favSet -> - val items = appRepository.apps( - sortOrder = sortOrder, - searchQuery = searchQuery, - categoriesToInclude = categories.toList(), - ) - if (favOnly) items.filter { it.packageName.name in favSet } else items - }.asStateFlow(emptyList()) + // TODO(sqldelight): reimplement with SQLDelight-backed repository + val appsState: StateFlow> = MutableStateFlow(emptyList()) fun toggleCategory(category: DefaultName) { val currentCategories = _selectedCategories.value diff --git a/app/src/main/kotlin/com/looker/droidify/compose/repoDetail/RepoDetailViewModel.kt b/app/src/main/kotlin/com/looker/droidify/compose/repoDetail/RepoDetailViewModel.kt index 760f3ef24..a57a4123b 100644 --- a/app/src/main/kotlin/com/looker/droidify/compose/repoDetail/RepoDetailViewModel.kt +++ b/app/src/main/kotlin/com/looker/droidify/compose/repoDetail/RepoDetailViewModel.kt @@ -2,36 +2,30 @@ package com.looker.droidify.compose.repoDetail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import com.looker.droidify.compose.repoDetail.navigation.RepoDetail -import com.looker.droidify.data.RepoRepository -import com.looker.droidify.utility.common.extension.asStateFlow +import com.looker.droidify.data.model.Repo import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import javax.inject.Inject @HiltViewModel class RepoDetailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - private val repoRepository: RepoRepository, ) : ViewModel() { private val route: RepoDetail = savedStateHandle.toRoute() val repoId = route.repoId - val repo = repoRepository.repo(repoId).asStateFlow(null) + // TODO(sqldelight): reimplement with SQLDelight-backed repository + val repo: StateFlow = MutableStateFlow(null) fun enableRepository(enable: Boolean) { - viewModelScope.launch { - repo.value?.let { repoRepository.enableRepository(it, enable) } - } + // TODO(sqldelight): reimplement with SQLDelight-backed repository } fun deleteRepository(onDelete: () -> Unit) { - viewModelScope.launch { - repoRepository.deleteRepo(repoId) - onDelete() - } + // TODO(sqldelight): reimplement with SQLDelight-backed repository } } diff --git a/app/src/main/kotlin/com/looker/droidify/compose/repoEdit/RepoEditViewModel.kt b/app/src/main/kotlin/com/looker/droidify/compose/repoEdit/RepoEditViewModel.kt index efd1550be..679c9629c 100644 --- a/app/src/main/kotlin/com/looker/droidify/compose/repoEdit/RepoEditViewModel.kt +++ b/app/src/main/kotlin/com/looker/droidify/compose/repoEdit/RepoEditViewModel.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.snapshotFlow import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.looker.droidify.data.RepoRepository import com.looker.droidify.network.Downloader import com.looker.droidify.network.NetworkResponse import com.looker.droidify.network.header.authentication @@ -15,7 +14,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.net.URI @@ -26,7 +24,6 @@ import javax.inject.Inject @HiltViewModel class RepoEditViewModel @Inject constructor( - private val repoRepository: RepoRepository, private val downloader: Downloader, ) : ViewModel() { @@ -46,9 +43,8 @@ class RepoEditViewModel @Inject constructor( private val _isLoading = MutableStateFlow(false) val isLoading: StateFlow = _isLoading - private val takenAddresses: StateFlow> = repoRepository.addresses.map { - it.map { address -> stripPathSuffix(address) }.toSet() - }.asStateFlow(emptySet()) + // TODO(sqldelight): reimplement with SQLDelight-backed repository + private val takenAddresses: StateFlow> = MutableStateFlow(emptySet()) private val addressFlow = snapshotFlow { addressState.text.toString() } private val fingerprintFlow = snapshotFlow { fingerprintState.text.toString() } @@ -73,21 +69,8 @@ class RepoEditViewModel @Inject constructor( private val addressSuffixes = arrayOf("fdroid/repo", "repo") fun loadRepo(repoId: Int) { - viewModelScope.launch { - _repoId.value = repoId - val repo = repoRepository.getRepo(repoId) - repo?.let { - addressState.edit { this.append(it.address) } - it.fingerprint?.let { fingerprint -> - fingerprintState.edit { this.append(formatFingerprint(fingerprint.value)) } - } - it.authentication?.let { auth -> - _authEnabled.value = true - usernameState.edit { this.append(auth.username) } - passwordState.edit { this.append(auth.password) } - } - } - } + _repoId.value = repoId + // TODO(sqldelight): reimplement with SQLDelight-backed repository } fun setAuthEnabled(enabled: Boolean) { @@ -195,14 +178,7 @@ class RepoEditViewModel @Inject constructor( username: String?, password: String?, ) { - viewModelScope.launch { - repoRepository.insertRepo( - address = address, - fingerprint = fingerprint.ifEmpty { null }, - username = username, - password = password, - ) - } + // TODO(sqldelight): reimplement with SQLDelight-backed repository } private fun addressError(address: String): String? { diff --git a/app/src/main/kotlin/com/looker/droidify/compose/repoList/RepoListViewModel.kt b/app/src/main/kotlin/com/looker/droidify/compose/repoList/RepoListViewModel.kt index f8bfd7323..694d3b084 100644 --- a/app/src/main/kotlin/com/looker/droidify/compose/repoList/RepoListViewModel.kt +++ b/app/src/main/kotlin/com/looker/droidify/compose/repoList/RepoListViewModel.kt @@ -1,31 +1,23 @@ package com.looker.droidify.compose.repoList import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.looker.droidify.data.RepoRepository import com.looker.droidify.data.model.Repo -import com.looker.droidify.utility.common.extension.asStateFlow import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import javax.inject.Inject @HiltViewModel -class RepoListViewModel @Inject constructor( - private val repository: RepoRepository, -) : ViewModel() { +class RepoListViewModel @Inject constructor() : ViewModel() { - val stream = repository.repos - .asStateFlow(emptyList()) + // TODO(sqldelight): reimplement with SQLDelight-backed repository + val stream: StateFlow> = MutableStateFlow(emptyList()) fun toggleRepo(repo: Repo) { - viewModelScope.launch { - repository.enableRepository(repo, !repo.enabled) - } + // TODO(sqldelight): reimplement with SQLDelight-backed repository } fun deleteRepo(repoId: Int) { - viewModelScope.launch { - repository.deleteRepo(repoId) - } + // TODO(sqldelight): reimplement with SQLDelight-backed repository } } diff --git a/app/src/main/kotlin/com/looker/droidify/data/AppRepository.kt b/app/src/main/kotlin/com/looker/droidify/data/AppRepository.kt deleted file mode 100644 index d3aa60511..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/AppRepository.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.looker.droidify.data - -import com.looker.droidify.data.local.dao.AppDao -import com.looker.droidify.data.local.dao.RepoDao -import com.looker.droidify.data.local.model.toApp -import com.looker.droidify.data.model.App -import com.looker.droidify.data.model.AppMinimal -import com.looker.droidify.data.model.PackageName -import com.looker.droidify.datastore.SettingsRepository -import com.looker.droidify.datastore.get -import com.looker.droidify.datastore.model.SortOrder -import com.looker.droidify.sync.v2.model.DefaultName -import com.looker.droidify.sync.v2.model.Tag -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import javax.inject.Inject - -class AppRepository @Inject constructor( - private val appDao: AppDao, - private val repoDao: RepoDao, - private val settingsRepository: SettingsRepository, -) { - - private val localeStream = settingsRepository.get { language } - - suspend fun apps( - sortOrder: SortOrder, - searchQuery: String? = null, - repoId: Int? = null, - categoriesToInclude: List? = null, - categoriesToExclude: List? = null, - antiFeaturesToInclude: List? = null, - antiFeaturesToExclude: List? = null, - ): List = withContext(Dispatchers.Default) { - val currentLocale = localeStream.first() - appDao.query( - sortOrder = sortOrder, - searchQuery = searchQuery?.ifEmpty { null }, - repoId = repoId, - categoriesToInclude = categoriesToInclude?.ifEmpty { null }, - categoriesToExclude = categoriesToExclude?.ifEmpty { null }, - antiFeaturesToInclude = antiFeaturesToInclude?.ifEmpty { null }, - antiFeaturesToExclude = antiFeaturesToExclude?.ifEmpty { null }, - locale = currentLocale, - ) - } - - val categories: Flow> - get() = repoDao.categories().map { it.map { category -> category.defaultName } } - - fun getApp(packageName: PackageName): Flow> = combine( - appDao.queryAppEntity(packageName.name), - localeStream, - ) { appEntityRelations, locale -> - appEntityRelations.map { - val repo = repoDao.getRepo(it.app.repoId)!! - it.toApp(locale, repo) - } - } - - suspend fun addToFavourite(packageName: PackageName): Boolean { - val favourites = settingsRepository.get { favouriteApps }.first() - val wasInFavourites = packageName.name in favourites - settingsRepository.toggleFavourites(packageName.name) - return !wasInFavourites - } -} diff --git a/app/src/main/kotlin/com/looker/droidify/data/InstalledRepository.kt b/app/src/main/kotlin/com/looker/droidify/data/InstalledRepository.kt deleted file mode 100644 index aae8b0b26..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/InstalledRepository.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.looker.droidify.data - -import com.looker.droidify.data.local.dao.InstalledDao -import com.looker.droidify.data.local.model.toDomain -import com.looker.droidify.data.local.model.toEntity -import com.looker.droidify.model.InstalledItem -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import javax.inject.Inject - -class InstalledRepository @Inject constructor( - private val installedDao: InstalledDao, -) { - - fun getStream(packageName: String): Flow { - return installedDao.stream(packageName).map { entity -> - entity?.toDomain() - } - } - - fun getAllStream(): Flow> { - return installedDao.streamAll().map { entities -> - entities.map { it.toDomain() } - } - } - - suspend fun get(packageName: String): InstalledItem? { - return installedDao.get(packageName)?.toDomain() - } - - suspend fun put(installedItem: InstalledItem) { - installedDao.insert(installedItem.toEntity()) - } - - suspend fun putAll(installedItems: List) { - installedDao.replaceAll(installedItems.map { it.toEntity() }) - } - - suspend fun delete(packageName: String): Int { - return installedDao.delete(packageName) - } -} diff --git a/app/src/main/kotlin/com/looker/droidify/data/RepoRepository.kt b/app/src/main/kotlin/com/looker/droidify/data/RepoRepository.kt deleted file mode 100644 index 10f312db6..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/RepoRepository.kt +++ /dev/null @@ -1,239 +0,0 @@ -package com.looker.droidify.data - -import android.content.Context -import com.looker.droidify.data.encryption.EncryptionStorage -import com.looker.droidify.data.local.dao.AppDao -import com.looker.droidify.data.local.dao.AuthDao -import com.looker.droidify.data.local.dao.IndexDao -import com.looker.droidify.data.local.dao.RepoDao -import com.looker.droidify.data.local.model.AuthenticationEntity -import com.looker.droidify.data.local.model.LocalizedRepoDescriptionEntity -import com.looker.droidify.data.local.model.LocalizedRepoNameEntity -import com.looker.droidify.data.local.model.RepoEntity -import com.looker.droidify.data.local.model.toAuthentication -import com.looker.droidify.data.local.model.toRepo -import com.looker.droidify.data.model.Fingerprint -import com.looker.droidify.data.model.Repo -import com.looker.droidify.datastore.SettingsRepository -import com.looker.droidify.datastore.get -import com.looker.droidify.di.IoDispatcher -import com.looker.droidify.network.Downloader -import com.looker.droidify.sync.LocalSyncable -import com.looker.droidify.sync.SyncState -import com.looker.droidify.sync.v1.V1Syncable -import com.looker.droidify.sync.v2.EntrySyncable -import com.looker.droidify.sync.v2.model.IndexV2 -import com.looker.droidify.work.SyncWorker -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.supervisorScope -import java.io.File -import javax.inject.Inject - -class RepoRepository @Inject constructor( - encryptionStorage: EncryptionStorage, - downloader: Downloader, - @param:ApplicationContext private val context: Context, - @IoDispatcher syncDispatcher: CoroutineDispatcher, - private val repoDao: RepoDao, - private val authDao: AuthDao, - private val indexDao: IndexDao, - private val settingsRepository: SettingsRepository, - private val appDao: AppDao, -) { - - private val localSyncable = LocalSyncable(context = context) - - private val v2Syncable = EntrySyncable( - context = context, - downloader = downloader, - dispatcher = syncDispatcher, - ) - - private val v1Syncable = V1Syncable( - context = context, - downloader = downloader, - dispatcher = syncDispatcher, - ) - - private val settings = settingsRepository.data - private val keyStream = encryptionStorage.key - private val locale = settings.map { it.language } - - suspend fun getRepo(id: Int): Repo? { - val repoEntity = repoDao.getRepo(id) ?: return null - val key = keyStream.first() - val auth = authDao.authFor(id)?.toAuthentication(key) - val currentLocale = locale.first() - val enabled = id in settings.first().enabledRepoIds - val mirrors = getMirrors(id) - val name = repoDao.name(id, currentLocale) ?: repoEntity.address - val description = repoDao.description(id, currentLocale) ?: "" - val icon = repoDao.icon(id, currentLocale)?.icon?.name - return repoEntity.toRepo( - mirrors = mirrors, - enabled = enabled, - authentication = auth, - name = name, - description = description, - icon = icon, - ) - } - - fun repo(id: Int): Flow = combine( - repoDao.repo(id), - settings.map { it.enabledRepoIds }, - keyStream, - ) { repo, enabled, key -> - val auth = authDao.authFor(id)?.toAuthentication(key) - val mirrors = getMirrors(id) - val currentLocale = locale.first() - val name = repoDao.name(id, currentLocale) ?: repo?.address ?: "Unknown" - val description = repoDao.description(id, currentLocale) ?: "" - val icon = repoDao.icon(id, currentLocale)?.icon?.name - repo?.toRepo( - mirrors = mirrors, - enabled = repo.id in enabled, - authentication = auth, - name = name, - description = description, - icon = icon, - ) - } - - suspend fun deleteRepo(id: Int) { - repoDao.delete(id) - } - - val repos: Flow> = combine( - repoDao.stream(), - settings.map { it.enabledRepoIds }, - ) { repos, enabledIds -> - val currentLocale = locale.first() - repos.map { repoEntity -> - val name = repoDao.name(repoEntity.id, currentLocale) ?: repoEntity.address - val description = repoDao.description(repoEntity.id, currentLocale) ?: "" - val icon = repoDao.icon(repoEntity.id, currentLocale)?.icon?.name - repoEntity.toRepo( - mirrors = emptyList(), - authentication = null, - enabled = repoEntity.id in enabledIds, - name = name, - description = description, - icon = icon, - ) - } - } - - val addresses: Flow> - get() = combine( - repoDao.stream(), - repoDao.mirrors(), - ) { repos, mirrors -> - repos.map { it.address }.toSet() + mirrors.map { it.url } - } - - fun getEnabledRepos(): Flow> = settingsRepository - .get { enabledRepoIds } - .map { ids -> ids.mapNotNull { repoId -> getRepo(repoId) } } - - suspend fun insertRepo( - address: String, - fingerprint: String?, - username: String?, - password: String?, - name: String? = null, - description: String? = null, - ) { - val id = indexDao.insertRepo( - RepoEntity( - address = address, - fingerprint = Fingerprint(fingerprint.orEmpty()), - timestamp = null, - webBaseUrl = address, - ), - ) - if (name != null) { - indexDao.insertLocalizedRepoNames( - listOf(LocalizedRepoNameEntity(id.toInt(), "en-US", name)), - ) - } - - if (description != null) { - indexDao.insertLocalizedRepoDescription( - listOf(LocalizedRepoDescriptionEntity(id.toInt(), "en-US", description)), - ) - } - if (password != null && username != null) { - val key = keyStream.first() - val (encrypted, iv) = key.encrypt(password) - val authEntity = AuthenticationEntity( - password = encrypted, - username = username, - initializationVector = iv, - repoId = id.toInt(), - ) - authDao.insert(authEntity) - } - } - - suspend fun enableRepository(repo: Repo, enable: Boolean) { - settingsRepository.setRepoEnabled(repo.id, enable) - if (enable) { - SyncWorker.syncRepo(context, repo.id) - } else { - repoDao.resetTimestamp(repo.id) - runCatching { - val indexDir = File(context.cacheDir, "index") - if (indexDir.exists()) { - indexDir.listFiles()?.forEach { file -> - if (file.name.startsWith("repo_${repo.id}_")) { - file.delete() - } - } - } - } - appDao.deleteByRepoId(repo.id) - } - } - - suspend fun sync(repo: Repo, onState: ((SyncState) -> Unit)? = null): Boolean { - var success = false - var parsedFingerprint: Fingerprint? = null - var parsedIndex: IndexV2? = null - v2Syncable.sync(repo) { state -> - onState?.invoke(state) - when (state) { - is SyncState.JsonParsing.Success -> { - parsedFingerprint = state.fingerprint - parsedIndex = state.index - success = true - } - - else -> Unit - } - } - if (parsedIndex != null && parsedFingerprint != null) { - indexDao.insertIndex( - fingerprint = parsedFingerprint, - index = parsedIndex!!, - expectedRepoId = repo.id, - ) - } - return success - } - - suspend fun syncAll(): Boolean = supervisorScope { - val repos = getEnabledRepos().first() - repos.forEach { repo -> launch { sync(repo) } } - true - } - - private suspend fun getMirrors(repoId: Int): List = - repoDao.mirrors(repoId).map { it.url } -} diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/DroidifyDatabase.kt b/app/src/main/kotlin/com/looker/droidify/data/local/DroidifyDatabase.kt deleted file mode 100644 index f905bf675..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/DroidifyDatabase.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.looker.droidify.data.local - -import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import androidx.sqlite.db.SupportSQLiteDatabase -import com.looker.droidify.data.local.converters.Converters -import com.looker.droidify.data.local.converters.PermissionConverter -import com.looker.droidify.data.local.dao.AppDao -import com.looker.droidify.data.local.dao.AuthDao -import com.looker.droidify.data.local.dao.IndexDao -import com.looker.droidify.data.local.dao.InstalledDao -import com.looker.droidify.data.local.dao.RepoDao -import com.looker.droidify.data.local.model.AntiFeatureAppRelation -import com.looker.droidify.data.local.model.AntiFeatureEntity -import com.looker.droidify.data.local.model.AntiFeatureRepoRelation -import com.looker.droidify.data.local.model.AppEntity -import com.looker.droidify.data.local.model.AuthenticationEntity -import com.looker.droidify.data.local.model.AuthorEntity -import com.looker.droidify.data.local.model.CategoryAppRelation -import com.looker.droidify.data.local.model.CategoryEntity -import com.looker.droidify.data.local.model.CategoryRepoRelation -import com.looker.droidify.data.local.model.DonateEntity -import com.looker.droidify.data.local.model.GraphicEntity -import com.looker.droidify.data.local.model.InstalledEntity -import com.looker.droidify.data.local.model.LinksEntity -import com.looker.droidify.data.local.model.LocalizedAppDescriptionEntity -import com.looker.droidify.data.local.model.LocalizedAppIconEntity -import com.looker.droidify.data.local.model.LocalizedAppNameEntity -import com.looker.droidify.data.local.model.LocalizedAppSummaryEntity -import com.looker.droidify.data.local.model.LocalizedRepoDescriptionEntity -import com.looker.droidify.data.local.model.LocalizedRepoIconEntity -import com.looker.droidify.data.local.model.LocalizedRepoNameEntity -import com.looker.droidify.data.local.model.MirrorEntity -import com.looker.droidify.data.local.model.RepoEntity -import com.looker.droidify.data.local.model.ScreenshotEntity -import com.looker.droidify.data.local.model.VersionEntity - -@Database( - version = 3, - exportSchema = true, - entities = [ - AntiFeatureEntity::class, - AntiFeatureAppRelation::class, - AntiFeatureRepoRelation::class, - AuthenticationEntity::class, - AuthorEntity::class, - AppEntity::class, - CategoryEntity::class, - CategoryAppRelation::class, - CategoryRepoRelation::class, - DonateEntity::class, - GraphicEntity::class, - InstalledEntity::class, - LinksEntity::class, - MirrorEntity::class, - RepoEntity::class, - ScreenshotEntity::class, - VersionEntity::class, - // Localized Data - LocalizedAppNameEntity::class, - LocalizedAppSummaryEntity::class, - LocalizedAppDescriptionEntity::class, - LocalizedAppIconEntity::class, - LocalizedRepoNameEntity::class, - LocalizedRepoDescriptionEntity::class, - LocalizedRepoIconEntity::class, - ], -) -@TypeConverters( - PermissionConverter::class, - Converters::class, -) -abstract class DroidifyDatabase : RoomDatabase() { - abstract fun appDao(): AppDao - abstract fun repoDao(): RepoDao - abstract fun authDao(): AuthDao - abstract fun indexDao(): IndexDao - abstract fun installedDao(): InstalledDao -} - -fun droidifyDatabase(context: Context): DroidifyDatabase = Room - .databaseBuilder( - context = context, - klass = DroidifyDatabase::class.java, - name = "droidify_room", - ) - .fallbackToDestructiveMigration(true) - .addCallback( - object : RoomDatabase.Callback() { - override fun onOpen(db: SupportSQLiteDatabase) { - super.onOpen(db) - db.query("PRAGMA synchronous = OFF") - db.query("PRAGMA journal_mode = WAL") - } - }, - ) - .build() diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/converters/Converters.kt b/app/src/main/kotlin/com/looker/droidify/data/local/converters/Converters.kt deleted file mode 100644 index 1075136bf..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/converters/Converters.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.looker.droidify.data.local.converters - -import androidx.room.TypeConverter -import com.looker.droidify.sync.JsonParser -import com.looker.droidify.sync.v2.model.LocalizedString -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.MapSerializer -import kotlinx.serialization.builtins.serializer - -private val localizedStringSerializer = MapSerializer(String.serializer(), String.serializer()) -private val stringListSerializer = ListSerializer(String.serializer()) - -object Converters { - - @TypeConverter - fun fromLocalizedString(value: LocalizedString): String { - return JsonParser.encodeToString(localizedStringSerializer, value) - } - - @TypeConverter - fun toLocalizedString(value: String): LocalizedString { - return JsonParser.decodeFromString(localizedStringSerializer, value) - } - - @TypeConverter - fun fromStringList(value: List): String { - return JsonParser.encodeToString(stringListSerializer, value) - } - - @TypeConverter - fun toStringList(value: String): List { - return JsonParser.decodeFromString(stringListSerializer, value) - } -} diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/converters/PermissionConverter.kt b/app/src/main/kotlin/com/looker/droidify/data/local/converters/PermissionConverter.kt deleted file mode 100644 index 7f2a670ff..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/converters/PermissionConverter.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.looker.droidify.data.local.converters - -import androidx.room.TypeConverter -import com.looker.droidify.sync.JsonParser -import com.looker.droidify.sync.v2.model.PermissionV2 -import kotlinx.serialization.builtins.ListSerializer - -private val permissionListSerializer = ListSerializer(PermissionV2.serializer()) - -object PermissionConverter { - - @TypeConverter - fun fromPermissionV2List(value: List): String { - return JsonParser.encodeToString(permissionListSerializer, value) - } - - @TypeConverter - fun toPermissionV2List(value: String): List { - return JsonParser.decodeFromString(permissionListSerializer, value) - } -} diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/dao/AppDao.kt b/app/src/main/kotlin/com/looker/droidify/data/local/dao/AppDao.kt deleted file mode 100644 index 301c7ae5b..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/dao/AppDao.kt +++ /dev/null @@ -1,281 +0,0 @@ -package com.looker.droidify.data.local.dao - -import androidx.room.Dao -import androidx.room.MapInfo -import androidx.room.Query -import androidx.room.RawQuery -import androidx.room.Transaction -import androidx.sqlite.db.SimpleSQLiteQuery -import com.looker.droidify.data.local.model.AntiFeatureAppRelation -import com.looker.droidify.data.local.model.AppEntity -import com.looker.droidify.data.local.model.AppEntityRelations -import com.looker.droidify.data.local.model.CategoryAppRelation -import com.looker.droidify.data.local.model.LocalizedAppIconEntity -import com.looker.droidify.data.local.model.VersionEntity -import com.looker.droidify.data.model.AppMinimal -import com.looker.droidify.data.model.FilePath -import com.looker.droidify.data.model.PackageName -import com.looker.droidify.datastore.model.SortOrder -import com.looker.droidify.sync.v2.model.DefaultName -import com.looker.droidify.sync.v2.model.Tag -import kotlinx.coroutines.flow.Flow - -@Dao -interface AppDao { - - @RawQuery( - observedEntities = [ - AppEntity::class, - VersionEntity::class, - CategoryAppRelation::class, - AntiFeatureAppRelation::class, - ], - ) - fun _rawStreamAppEntities(query: SimpleSQLiteQuery): Flow> - - @RawQuery - suspend fun _rawQueryAppEntities(query: SimpleSQLiteQuery): List - - suspend fun query( - sortOrder: SortOrder, - searchQuery: String? = null, - repoId: Int? = null, - categoriesToInclude: List? = null, - categoriesToExclude: List? = null, - antiFeaturesToInclude: List? = null, - antiFeaturesToExclude: List? = null, - locale: String, - ): List = _rawQueryAppMinimal( - searchQueryMinimal( - sortOrder = sortOrder, - searchQuery = searchQuery, - repoId = repoId, - categoriesToInclude = categoriesToInclude, - categoriesToExclude = categoriesToExclude, - antiFeaturesToInclude = antiFeaturesToInclude, - antiFeaturesToExclude = antiFeaturesToExclude, - locale = locale, - ), - ).map { - AppMinimal( - appId = it.appId.toLong(), - packageName = PackageName(it.packageName), - name = it.name, - summary = it.summary, - icon = FilePath(it.baseAddress, it.iconName), - suggestedVersion = it.suggestedVersion ?: "", - ) - } - - data class AppMinimalRow( - val appId: Int, - val packageName: String, - val name: String, - val summary: String?, - val baseAddress: String, - val iconName: String?, - val suggestedVersion: String?, - ) - - @RawQuery - suspend fun _rawQueryAppMinimal(query: SimpleSQLiteQuery): List - - private fun searchQueryMinimal( - sortOrder: SortOrder, - searchQuery: String?, - repoId: Int?, - categoriesToInclude: List?, - categoriesToExclude: List?, - antiFeaturesToInclude: List?, - antiFeaturesToExclude: List?, - locale: String, - ): SimpleSQLiteQuery { - logQuery( - "sortOrder" to sortOrder, - "searchQuery" to searchQuery, - "repoId" to repoId, - "categoriesToInclude" to categoriesToInclude, - "categoriesToExclude" to categoriesToExclude, - "antiFeaturesToInclude" to antiFeaturesToInclude, - "antiFeaturesToExclude" to antiFeaturesToExclude, - "locale" to locale, - ) - val args = arrayListOf() - - val query = buildString(2048) { - append( - """ - SELECT - app.id AS appId, - app.packageName AS packageName, - COALESCE(n_loc.name, n_en.name) AS name, - COALESCE(s_loc.summary, s_en.summary) AS summary, - repo.address AS baseAddress, - COALESCE(i_loc.icon_name, i_en.icon_name) AS iconName, - ( - SELECT v.versionName FROM version v - WHERE v.appId = app.id - ORDER BY v.versionCode DESC - LIMIT 1 - ) AS suggestedVersion - FROM app - JOIN repository AS repo ON app.repoId = repo.id - LEFT JOIN localized_app_name AS n_loc ON n_loc.appId = app.id AND n_loc.locale = ? - LEFT JOIN localized_app_name AS n_en ON n_en.appId = app.id AND n_en.locale = 'en-US' - LEFT JOIN localized_app_summary AS s_loc ON s_loc.appId = app.id AND s_loc.locale = ? - LEFT JOIN localized_app_summary AS s_en ON s_en.appId = app.id AND s_en.locale = 'en-US' - LEFT JOIN localized_app_icon AS i_loc ON i_loc.appId = app.id AND i_loc.locale = ? - LEFT JOIN localized_app_icon AS i_en ON i_en.appId = app.id AND i_en.locale = 'en-US' - LEFT JOIN localized_app_description AS d_loc ON d_loc.appId = app.id AND d_loc.locale = ? - LEFT JOIN localized_app_description AS d_en ON d_en.appId = app.id AND d_en.locale = 'en-US' - """.trimIndent(), - ) - // locale args repeated for each localized table - args.add(locale) - args.add(locale) - args.add(locale) - args.add(locale) - - if (sortOrder == SortOrder.SIZE) { - append(" LEFT JOIN version ON app.id = version.appId") - } - if (categoriesToInclude != null || categoriesToExclude != null) { - append(" LEFT JOIN category_app_relation ON app.id = category_app_relation.id") - } - if (antiFeaturesToExclude != null || antiFeaturesToInclude != null) { - append(" LEFT JOIN anti_features_app_relation ON app.id = anti_features_app_relation.appId") - } - - append(" WHERE 1") - - if (repoId != null) { - append(" AND app.repoId = ?") - args.add(repoId) - } - - if (categoriesToInclude != null) { - append(" AND category_app_relation.defaultName IN (") - append(categoriesToInclude.joinToString(", ") { "?" }) - append(")") - args.addAll(categoriesToInclude) - } - - if (categoriesToExclude != null) { - append(" AND category_app_relation.defaultName NOT IN (") - append(categoriesToExclude.joinToString(", ") { "?" }) - append(")") - args.addAll(categoriesToExclude) - } - - if (antiFeaturesToInclude != null) { - append(" AND anti_features_app_relation.tag IN (") - append(antiFeaturesToInclude.joinToString(", ") { "?" }) - append(")") - args.addAll(antiFeaturesToInclude) - } - - if (antiFeaturesToExclude != null) { - append(" AND anti_features_app_relation.tag NOT IN (") - append(antiFeaturesToExclude.joinToString(", ") { "?" }) - append(")") - args.addAll(antiFeaturesToExclude) - } - - if (searchQuery != null) { - val searchPattern = "%$searchQuery%" - append( - """ - AND ( - app.packageName LIKE ? - OR COALESCE(n_loc.name, n_en.name) LIKE ? - OR COALESCE(s_loc.summary, s_en.summary) LIKE ? - OR COALESCE(d_loc.description, d_en.description) LIKE ? - ) - """.trimIndent(), - ) - args.addAll(listOf(searchPattern, searchPattern, searchPattern, searchPattern)) - } - append(" GROUP BY app.packageName") - - append(" ORDER BY ") - - if (searchQuery != null) { - val searchPattern = "%$searchQuery%" - append("(CASE WHEN COALESCE(n_loc.name, n_en.name) LIKE ? THEN 4 ELSE 0 END) + ") - append("(CASE WHEN COALESCE(s_loc.summary, s_en.summary) LIKE ? THEN 3 ELSE 0 END) + ") - append("(CASE WHEN app.packageName LIKE ? THEN 2 ELSE 0 END) + ") - append("(CASE WHEN COALESCE(d_loc.description, d_en.description) LIKE ? THEN 1 ELSE 0 END) DESC, ") - args.addAll(listOf(searchPattern, searchPattern, searchPattern, searchPattern)) - } - - when (sortOrder) { - SortOrder.UPDATED -> append("app.lastUpdated DESC") - SortOrder.ADDED -> append("app.added DESC") - SortOrder.SIZE -> append("version.apk_size DESC") - SortOrder.NAME -> Unit - } - } - - return SimpleSQLiteQuery(query, args.toTypedArray()) - } - - @Query( - """ - SELECT app.* - FROM app - LEFT JOIN installed - ON app.packageName = installed.packageName - LEFT JOIN version - ON version.appId = app.id - WHERE installed.packageName IS NOT NULL - ORDER BY - CASE WHEN version.versionCode > installed.versionCode THEN 1 ELSE 2 END, - app.lastUpdated DESC - """, - ) - fun installedStream(): Flow> - - @Query("SELECT versionCode FROM version WHERE appId = :appId ORDER BY versionCode DESC LIMIT 1") - suspend fun suggestedVersionCode(appId: Int): Long - - @Query("SELECT versionName FROM version WHERE appId = :appId ORDER BY versionCode DESC LIMIT 1") - suspend fun suggestedVersionName(appId: Int): String - - // Batch fetch suggested (max versionCode) versionName for multiple appIds - @MapInfo(keyColumn = "appId", valueColumn = "versionName") - @Query( - """ - SELECT v.appId AS appId, MAX(v.versionName) AS versionName - FROM version v - GROUP BY appId - """, - ) - suspend fun suggestedVersionNamesAll(): Map - - @Transaction - @Query("SELECT * FROM app WHERE packageName = :packageName") - fun queryAppEntity(packageName: String): Flow> - - @Query("SELECT COUNT(*) FROM app") - suspend fun count(): Int - - @Query("DELETE FROM app WHERE id = :id") - suspend fun delete(id: Int) - - @Query("DELETE FROM app WHERE repoId = :repoId") - suspend fun deleteByRepoId(repoId: Int) - - @Query("SELECT name FROM localized_app_name WHERE appId = :id AND (locale = :locale OR locale = \'en-US\')") - suspend fun name(id: Int, locale: String): String? - - @Query("SELECT summary FROM localized_app_summary WHERE appId = :id AND (locale = :locale OR locale = \'en-US\')") - suspend fun summary(id: Int, locale: String): String? - - @Query( - "SELECT description FROM localized_app_description WHERE appId = :id AND (locale = :locale OR locale = \'en-US\')", - ) - suspend fun description(id: Int, locale: String): String? - - @Query("SELECT * FROM localized_app_icon WHERE appId = :id AND (locale = :locale OR locale = \'en-US\')") - suspend fun icon(id: Int, locale: String): LocalizedAppIconEntity? -} diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/dao/AuthDao.kt b/app/src/main/kotlin/com/looker/droidify/data/local/dao/AuthDao.kt deleted file mode 100644 index 428ec2a37..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/dao/AuthDao.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.looker.droidify.data.local.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.looker.droidify.data.local.model.AuthenticationEntity - -@Dao -interface AuthDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(authentication: AuthenticationEntity) - - @Query("SELECT * FROM authentication WHERE repoId = :repoId") - suspend fun authFor(repoId: Int): AuthenticationEntity? -} diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/dao/IndexDao.kt b/app/src/main/kotlin/com/looker/droidify/data/local/dao/IndexDao.kt deleted file mode 100644 index 738d499ba..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/dao/IndexDao.kt +++ /dev/null @@ -1,363 +0,0 @@ -package com.looker.droidify.data.local.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Transaction -import androidx.room.Update -import androidx.room.Upsert -import com.looker.droidify.data.local.model.AntiFeatureAppRelation -import com.looker.droidify.data.local.model.AntiFeatureEntity -import com.looker.droidify.data.local.model.AntiFeatureRepoRelation -import com.looker.droidify.data.local.model.AppEntity -import com.looker.droidify.data.local.model.AuthorEntity -import com.looker.droidify.data.local.model.CategoryAppRelation -import com.looker.droidify.data.local.model.CategoryEntity -import com.looker.droidify.data.local.model.CategoryRepoRelation -import com.looker.droidify.data.local.model.DonateEntity -import com.looker.droidify.data.local.model.GraphicEntity -import com.looker.droidify.data.local.model.LinksEntity -import com.looker.droidify.data.local.model.LocalizedAppDescriptionEntity -import com.looker.droidify.data.local.model.LocalizedAppIconEntity -import com.looker.droidify.data.local.model.LocalizedAppNameEntity -import com.looker.droidify.data.local.model.LocalizedAppSummaryEntity -import com.looker.droidify.data.local.model.LocalizedRepoDescriptionEntity -import com.looker.droidify.data.local.model.LocalizedRepoIconEntity -import com.looker.droidify.data.local.model.LocalizedRepoNameEntity -import com.looker.droidify.data.local.model.MirrorEntity -import com.looker.droidify.data.local.model.RepoEntity -import com.looker.droidify.data.local.model.ScreenshotEntity -import com.looker.droidify.data.local.model.VersionEntity -import com.looker.droidify.data.local.model.antiFeatureEntity -import com.looker.droidify.data.local.model.appEntity -import com.looker.droidify.data.local.model.authorEntity -import com.looker.droidify.data.local.model.categoryEntity -import com.looker.droidify.data.local.model.donateEntity -import com.looker.droidify.data.local.model.linkEntity -import com.looker.droidify.data.local.model.localizedGraphics -import com.looker.droidify.data.local.model.localizedScreenshots -import com.looker.droidify.data.local.model.mirrorEntity -import com.looker.droidify.data.local.model.repoEntity -import com.looker.droidify.data.local.model.versionEntities -import com.looker.droidify.data.model.Fingerprint -import com.looker.droidify.sync.v2.model.IndexV2 -import com.looker.droidify.sync.v2.model.LocalizedIcon -import com.looker.droidify.sync.v2.model.LocalizedString - -@Dao -interface IndexDao { - - @Transaction - suspend fun insertIndex( - fingerprint: Fingerprint, - index: IndexV2, - expectedRepoId: Int = 0, - ) { - val repo = index.repo.repoEntity(id = expectedRepoId, fingerprint = fingerprint) - val repoId = upsertRepo(repo) - insertRepoScopeData(repoId, index) - - val packageEntries = index.packages.entries.toList() - - val authorIdsByAuthor = mutableMapOf() - packageEntries.asSequence() - .map { it.value.metadata.authorEntity() } - .distinct() - .forEach { author -> - val id = upsertAuthor(author) - authorIdsByAuthor[author] = id - } - - val appEntities = packageEntries.map { (packageName, packages) -> - val authorId = authorIdsByAuthor[packages.metadata.authorEntity()]!! - packages.metadata.appEntity( - packageName = packageName, - repoId = repoId, - authorId = authorId, - ) - } - - val packageNames = packageEntries.map { it.key } - val existing = appIdsByPackageNames(repoId, packageNames) - val existingIdByPackage = mutableMapOf().apply { - existing.forEach { put(it.packageName, it.id) } - } - - val toUpdate = appEntities.filter { existingIdByPackage.containsKey(it.packageName) } - val toInsert = appEntities.filter { !existingIdByPackage.containsKey(it.packageName) } - - if (toUpdate.isNotEmpty()) upsertApps(toUpdate) - val insertedIds: Map = if (toInsert.isNotEmpty()) { - val result = insertApps(toInsert) - toInsert.mapIndexed { idx, entity -> entity.packageName to result[idx].toInt() }.toMap() - } else { - emptyMap() - } - - val appIdByPackage: Map = existingIdByPackage + insertedIds - - val allVersions = mutableListOf() - val allAntiFeatureAppRelations = mutableListOf() - val allCategoryAppRelations = mutableListOf() - - val allAppNames = mutableListOf() - val allAppSummaries = mutableListOf() - val allAppDescriptions = mutableListOf() - val allAppIcons = mutableListOf() - - val allLinks = mutableListOf() - val allScreenshots = mutableListOf() - val allGraphics = mutableListOf() - val allDonations = mutableListOf() - - packageEntries.forEach { (packageName, packages) -> - val appId = appIdByPackage.getValue(packageName) - val metadata = packages.metadata - - val versionsMap = packages.versionEntities(appId) - allVersions += versionsMap.keys - allAntiFeatureAppRelations += versionsMap.values.flatten() - - allCategoryAppRelations += metadata.categories.map { CategoryAppRelation(appId, it) } - - allAppNames += metadata.name?.localizedAppName(appId) - ?: listOf(LocalizedAppNameEntity(appId, locale = "en-US", name = packageName)) - metadata.summary?.localizedAppSummary(appId)?.let { allAppSummaries += it } - metadata.description?.localizedAppDescription(appId)?.let { allAppDescriptions += it } - metadata.icon?.localizedAppIcon(appId)?.let { allAppIcons += it } - - metadata.linkEntity(appId)?.let { allLinks += it } - metadata.screenshots?.localizedScreenshots(appId)?.let { allScreenshots += it } - metadata.localizedGraphics(appId)?.let { allGraphics += it } - metadata.donateEntity(appId)?.let { allDonations += it } - } - - if (allVersions.isNotEmpty()) insertVersions(allVersions) - if (allAntiFeatureAppRelations.isNotEmpty()) { - insertAntiFeatureAppRelation( - allAntiFeatureAppRelations, - ) - } - if (allCategoryAppRelations.isNotEmpty()) insertCategoryAppRelation(allCategoryAppRelations) - - insertLocalizedAppData( - names = allAppNames, - summaries = allAppSummaries.ifEmpty { null }, - descriptions = allAppDescriptions.ifEmpty { null }, - icons = allAppIcons.ifEmpty { null }, - ) - - if (allLinks.isNotEmpty()) insertLinks(allLinks) - if (allScreenshots.isNotEmpty()) insertScreenshots(allScreenshots) - if (allGraphics.isNotEmpty()) insertGraphics(allGraphics) - if (allDonations.isNotEmpty()) insertDonate(allDonations) - } - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertRepo(repoEntity: RepoEntity): Long - - @Update(onConflict = OnConflictStrategy.REPLACE) - suspend fun updateRepo(repoEntity: RepoEntity) - - @Transaction - suspend fun upsertRepo(repoEntity: RepoEntity): Int { - val id = insertRepo(repoEntity) - return if (id == -1L) { - repoEntity.also { updateRepo(it) }.id - } else { - id.toInt() - } - } - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertMirror(mirrors: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertAntiFeatures(antiFeatures: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertCategories(categories: List) - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertAntiFeatureRepoRelation(crossRef: List) - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertCategoryRepoRelation(crossRef: List) - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertApp(appEntity: AppEntity): Long - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertApps(apps: List): List - - @Upsert - suspend fun upsertApp(appEntity: AppEntity) - - @Upsert - suspend fun upsertApps(apps: List) - - @Query("SELECT id FROM app WHERE packageName = :packageName AND repoId = :repoId LIMIT 1") - suspend fun appIdByPackageName(repoId: Int, packageName: String): Int? - - @Query("SELECT id, packageName FROM app WHERE repoId = :repoId AND packageName IN (:packageNames)") - suspend fun appIdsByPackageNames(repoId: Int, packageNames: List): List - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertAuthor(authorEntity: AuthorEntity): Long - - @Query( - """ - SELECT id FROM author - WHERE - (:email IS NULL AND email IS NULL OR email = :email) AND - (:name IS NULL AND name IS NULL OR name = :name COLLATE NOCASE) AND - (:website IS NULL AND website IS NULL OR website = :website COLLATE NOCASE) - LIMIT 1 - """, - ) - suspend fun authorId( - email: String?, - name: String?, - website: String?, - ): Int? - - @Transaction - suspend fun upsertAuthor(authorEntity: AuthorEntity): Int { - val id = insertAuthor(authorEntity) - return if (id == -1L) { - authorId( - email = authorEntity.email, - name = authorEntity.name, - website = authorEntity.website, - )!! - } else { - id.toInt() - } - } - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertScreenshots(screenshotEntity: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertLink(linksEntity: LinksEntity) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertGraphics(graphicEntity: List) - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertDonate(donateEntity: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertVersions(versions: List) - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertCategoryAppRelation(crossRef: List) - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertAntiFeatureAppRelation(crossRef: List) - - @Transaction - suspend fun insertLocalizedRepoData( - names: List, - descriptions: List, - icons: List?, - ) { - if (names.isNotEmpty()) insertLocalizedRepoNames(names) - if (descriptions.isNotEmpty()) insertLocalizedRepoDescription(descriptions) - if (!icons.isNullOrEmpty()) insertLocalizedRepoIcons(icons) - } - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertLocalizedRepoNames(names: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertLocalizedRepoDescription(descriptions: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertLocalizedRepoIcons(icons: List) - - @Transaction - suspend fun insertLocalizedAppData( - names: List, - summaries: List?, - descriptions: List?, - icons: List?, - ) { - if (names.isNotEmpty()) insertLocalizedAppNames(names) - if (!summaries.isNullOrEmpty()) insertLocalizedAppSummaries(summaries) - if (!descriptions.isNullOrEmpty()) insertLocalizedAppDescriptions(descriptions) - if (!icons.isNullOrEmpty()) insertLocalizedAppIcons(icons) - } - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertLocalizedAppNames(names: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertLocalizedAppSummaries(summaries: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertLocalizedAppDescriptions(descriptions: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertLocalizedAppIcons(icons: List) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertLinks(links: List) - - @Transaction - suspend fun insertRepoScopeData(repoId: Int, index: IndexV2) { - val antiFeatures = index.repo.antiFeatures.flatMap { (tag, feature) -> - feature.antiFeatureEntity(tag) - } - val antiFeatureRepoRelations = antiFeatures.map { AntiFeatureRepoRelation(repoId, it.tag) } - if (antiFeatures.isNotEmpty()) insertAntiFeatures(antiFeatures) - if (antiFeatureRepoRelations.isNotEmpty()) { - insertAntiFeatureRepoRelation( - antiFeatureRepoRelations, - ) - } - - val categories = index.repo.categories.flatMap { (defaultName, category) -> - category.categoryEntity(defaultName) - } - val categoryRepoRelations = categories.map { CategoryRepoRelation(repoId, it.defaultName) } - if (categories.isNotEmpty()) insertCategories(categories) - if (categoryRepoRelations.isNotEmpty()) insertCategoryRepoRelation(categoryRepoRelations) - - val mirrors = index.repo.mirrors.map { it.mirrorEntity(repoId) } - if (mirrors.isNotEmpty()) insertMirror(mirrors) - - insertLocalizedRepoData( - names = index.repo.name.localizedRepoName(repoId), - descriptions = index.repo.description.localizedRepoDescription(repoId), - icons = index.repo.icon?.localizedRepoIcon(repoId), - ) - } -} - -fun LocalizedString.localizedRepoName(repoId: Int) = - map { LocalizedRepoNameEntity(repoId, it.key, it.value) } - -fun LocalizedString.localizedRepoDescription(repoId: Int) = - map { LocalizedRepoDescriptionEntity(repoId, it.key, it.value.replace("\n", "
")) } - -fun LocalizedIcon.localizedRepoIcon(repoId: Int) = - map { LocalizedRepoIconEntity(repoId, it.key, it.value) } - -fun LocalizedString.localizedAppName(appId: Int) = - map { LocalizedAppNameEntity(appId, it.key, it.value) } - -fun LocalizedString.localizedAppSummary(appId: Int) = - map { LocalizedAppSummaryEntity(appId, it.key, it.value) } - -fun LocalizedString.localizedAppDescription(appId: Int) = - map { LocalizedAppDescriptionEntity(appId, it.key, it.value.replace("\n", "
")) } - -fun LocalizedIcon.localizedAppIcon(appId: Int) = - map { LocalizedAppIconEntity(appId, it.key, it.value) } - -data class AppIdPackage( - val id: Int, - val packageName: String, -) diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/dao/InstalledDao.kt b/app/src/main/kotlin/com/looker/droidify/data/local/dao/InstalledDao.kt deleted file mode 100644 index ae7646c99..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/dao/InstalledDao.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.looker.droidify.data.local.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Transaction -import com.looker.droidify.data.local.model.InstalledEntity -import kotlinx.coroutines.flow.Flow - -/** - * Data Access Object for installed applications. - * Provides methods to interact with the installed table in the database. - */ -@Dao -interface InstalledDao { - - /** - * Get an installed app by package name as a Flow. - * @param packageName The package name of the app. - * @return A Flow emitting the installed app or null if not found. - */ - @Query("SELECT * FROM installed WHERE packageName = :packageName") - fun stream(packageName: String): Flow - - /** - * Get all installed apps as a Flow. - * @return A Flow emitting a list of all installed apps. - */ - @Query("SELECT * FROM installed") - fun streamAll(): Flow> - - /** - * Get an installed app by package name. - * @param packageName The package name of the app. - * @return The installed app or null if not found. - */ - @Query("SELECT * FROM installed WHERE packageName = :packageName") - suspend fun get(packageName: String): InstalledEntity? - - /** - * Insert or update an installed app. - * @param installedEntity The installed app to insert or update. - */ - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(installedEntity: InstalledEntity) - - /** - * Insert or update multiple installed apps. - * @param installedEntities The list of installed apps to insert or update. - */ - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertAll(installedEntities: List) - - /** - * Replace all installed apps with a new list. - * @param installedEntities The new list of installed apps. - */ - @Transaction - suspend fun replaceAll(installedEntities: List) { - deleteAll() - insertAll(installedEntities) - } - - /** - * Delete an installed app by package name. - * @param packageName The package name of the app to delete. - * @return The number of rows affected. - */ - @Query("DELETE FROM installed WHERE packageName = :packageName") - suspend fun delete(packageName: String): Int - - /** - * Delete all installed apps. - */ - @Query("DELETE FROM installed") - suspend fun deleteAll() -} diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/dao/LogQueries.kt b/app/src/main/kotlin/com/looker/droidify/data/local/dao/LogQueries.kt deleted file mode 100644 index 153b40d97..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/dao/LogQueries.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.looker.droidify.data.local.dao - -import android.util.Log - -fun logQuery(vararg param: Pair) { - param.forEach { (key, value) -> - } - val message = buildString { - appendLine("(") - param.forEachIndexed { index, (key, value) -> - appendLine("\t$key: $value,") - } - appendLine(")") - } - Log.d("RoomQuery", message) -} diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/dao/RepoDao.kt b/app/src/main/kotlin/com/looker/droidify/data/local/dao/RepoDao.kt deleted file mode 100644 index a87bead1e..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/dao/RepoDao.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.looker.droidify.data.local.dao - -import androidx.room.Dao -import androidx.room.MapColumn -import androidx.room.Query -import androidx.room.RewriteQueriesToDropUnusedColumns -import com.looker.droidify.data.local.model.CategoryEntity -import com.looker.droidify.data.local.model.LocalizedRepoIconEntity -import com.looker.droidify.data.local.model.MirrorEntity -import com.looker.droidify.data.local.model.RepoEntity -import kotlinx.coroutines.flow.Flow - -@Dao -interface RepoDao { - - @Query("SELECT * FROM repository") - fun stream(): Flow> - - @Query("SELECT * FROM repository WHERE id = :repoId") - fun repo(repoId: Int): Flow - - @Query("SELECT * FROM repository WHERE id = :repoId") - suspend fun getRepo(repoId: Int): RepoEntity? - - @Query("SELECT id, address FROM repository WHERE id IN (:ids)") - suspend fun getAddressByIds(ids: List): Map< - @MapColumn("id") - Int, - @MapColumn("address") - String, - > - - @Query("SELECT * FROM category GROUP BY category.defaultName") - fun categories(): Flow> - - @Query( - """ - SELECT * FROM category - JOIN category_repo_relation ON category.defaultName = category_repo_relation.defaultName - WHERE category_repo_relation.id = :repoId - """, - ) - @RewriteQueriesToDropUnusedColumns - fun categoriesByRepoId(repoId: Int): Flow> - - @Query("SELECT * FROM mirror WHERE repoId = :repoId") - suspend fun mirrors(repoId: Int): List - - @Query("SELECT * FROM mirror") - fun mirrors(): Flow> - - @Query("UPDATE repository SET timestamp = NULL WHERE id = :id") - suspend fun resetTimestamp(id: Int) - - @Query("DELETE FROM repository WHERE id = :id") - suspend fun delete(id: Int) - - @Query("SELECT name FROM localized_repo_name WHERE repoId = :id AND (locale = :locale OR locale = \'en-US\')") - suspend fun name(id: Int, locale: String): String? - - @Query( - "SELECT description FROM localized_repo_description WHERE repoId = :id AND (locale = :locale OR locale = \'en-US\')", - ) - suspend fun description(id: Int, locale: String): String? - - @Query("SELECT * FROM localized_repo_icon WHERE repoId = :id AND (locale = :locale OR locale = \'en-US\')") - suspend fun icon(id: Int, locale: String): LocalizedRepoIconEntity? -} diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/AntiFeatureEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/AntiFeatureEntity.kt deleted file mode 100644 index 39ec57451..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/AntiFeatureEntity.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Index -import com.looker.droidify.sync.v2.model.AntiFeatureReason -import com.looker.droidify.sync.v2.model.AntiFeatureV2 -import com.looker.droidify.sync.v2.model.Tag - -@Entity( - tableName = "anti_feature", - primaryKeys = ["tag", "locale"], -) -data class AntiFeatureEntity( - val icon: String?, - val name: String, - val description: String?, - val locale: String, - val tag: Tag, -) - -@Entity( - tableName = "anti_feature_repo_relation", - primaryKeys = ["id", "tag"], - foreignKeys = [ - ForeignKey( - entity = RepoEntity::class, - childColumns = ["id"], - parentColumns = ["id"], - onDelete = ForeignKey.CASCADE, - ), - ], -) -data class AntiFeatureRepoRelation( - @ColumnInfo("id") - val repoId: Int, - val tag: Tag, -) - -@Entity( - tableName = "anti_features_app_relation", - primaryKeys = ["tag", "appId", "versionCode"], - indices = [Index("appId")], - foreignKeys = [ - ForeignKey( - entity = AppEntity::class, - childColumns = ["appId"], - parentColumns = ["id"], - onDelete = ForeignKey.CASCADE, - ), - ], -) -data class AntiFeatureAppRelation( - val tag: Tag, - val reason: AntiFeatureReason, - val appId: Int, - val versionCode: Long, -) - -fun AntiFeatureV2.antiFeatureEntity( - tag: Tag, -): List { - return name.map { (locale, localizedName) -> - AntiFeatureEntity( - icon = icon[locale]?.name, - name = localizedName, - description = description[locale], - tag = tag, - locale = locale, - ) - } -} diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/AppEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/AppEntity.kt deleted file mode 100644 index eaa65d04d..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/AppEntity.kt +++ /dev/null @@ -1,178 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.Companion.CASCADE -import androidx.room.Index -import androidx.room.Junction -import androidx.room.PrimaryKey -import androidx.room.Relation -import com.looker.droidify.data.model.App -import com.looker.droidify.data.model.FilePath -import com.looker.droidify.data.model.Html -import com.looker.droidify.data.model.Metadata -import com.looker.droidify.data.model.PackageName -import com.looker.droidify.sync.v2.model.MetadataV2 - -@Entity( - tableName = "app", - indices = [ - Index("authorId"), - Index("repoId"), - Index("packageName"), - Index("packageName", "repoId", unique = true), - ], - foreignKeys = [ - ForeignKey( - entity = RepoEntity::class, - childColumns = ["repoId"], - parentColumns = ["id"], - onDelete = CASCADE, - ), - ForeignKey( - entity = AuthorEntity::class, - childColumns = ["authorId"], - parentColumns = ["id"], - onDelete = CASCADE, - ), - ], -) -data class AppEntity( - val added: Long, - val lastUpdated: Long, - val license: String?, - val preferredSigner: String?, - val packageName: String, - val authorId: Int, - val repoId: Int, - @PrimaryKey(autoGenerate = true) - val id: Int = 0, -) - -data class AppEntityRelations( - @Embedded val app: AppEntity, - @Relation( - parentColumn = "authorId", - entityColumn = "id", - ) - val author: AuthorEntity, - @Relation( - parentColumn = "id", - entityColumn = "appId", - ) - val names: List, - @Relation( - parentColumn = "id", - entityColumn = "appId", - ) - val summaries: List, - @Relation( - parentColumn = "id", - entityColumn = "appId", - ) - val descriptions: List, - @Relation( - parentColumn = "id", - entityColumn = "appId", - ) - val icons: List, - @Relation( - parentColumn = "id", - entityColumn = "appId", - ) - val links: LinksEntity?, - @Relation( - parentColumn = "id", - entityColumn = "defaultName", - associateBy = Junction(CategoryAppRelation::class), - ) - val categories: List, - @Relation( - parentColumn = "id", - entityColumn = "appId", - ) - val donation: List?, - @Relation( - parentColumn = "id", - entityColumn = "appId", - ) - val graphics: List?, - @Relation( - parentColumn = "id", - entityColumn = "appId", - ) - val screenshots: List?, - @Relation( - parentColumn = "id", - entityColumn = "appId", - ) - val versions: List?, - @Relation( - parentColumn = "packageName", - entityColumn = "packageName", - ) - val installed: InstalledEntity?, -) - -fun MetadataV2.appEntity( - packageName: String, - repoId: Int, - authorId: Int, -) = AppEntity( - added = added, - lastUpdated = lastUpdated, - license = license, - preferredSigner = preferredSigner, - packageName = packageName, - authorId = authorId, - repoId = repoId, -) - -private fun AppEntity.toMetadata( - appName: String, - appSummary: String, - appDescription: String, - iconUrl: String?, - baseAddress: String, - versions: List?, -): Metadata { - val suggestedVersion = versions?.maxByOrNull { it.versionCode } - - return Metadata( - name = appName, - packageName = PackageName(packageName), - added = added, - description = Html(appDescription), - icon = FilePath(baseAddress, iconUrl), - lastUpdated = lastUpdated, - license = license ?: "Unknown", - suggestedVersionCode = suggestedVersion?.versionCode ?: 0, - suggestedVersionName = suggestedVersion?.versionName ?: "", - summary = appSummary, - ) -} - -fun AppEntityRelations.toApp( - locale: String, - repo: RepoEntity, -) = App( - repoId = app.repoId.toLong(), - appId = app.id.toLong(), - categories = categories.filterLocalized(locale).map { it.defaultName }, - links = links?.toLinks(), - metadata = app.toMetadata( - baseAddress = repo.address, - versions = versions, - appName = names.findLocale(locale).name, - appDescription = if (descriptions.isEmpty()) "" else descriptions.findLocale(locale).description, - iconUrl = icons.ifEmpty { null }?.findLocale(locale)?.icon?.name, - appSummary = if (summaries.isEmpty()) "" else summaries.findLocale(locale).summary, - ), - author = author.toAuthor(), - screenshots = screenshots?.toScreenshots(locale, repo.address), - graphics = graphics?.toGraphics(locale, repo.address), - donation = donation?.toDonation(), - preferredSigner = app.preferredSigner ?: "", - packages = versions?.toPackages(locale, installed), -) diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/AuthenticationEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/AuthenticationEntity.kt deleted file mode 100644 index 486055db4..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/AuthenticationEntity.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.Companion.CASCADE -import androidx.room.PrimaryKey -import com.looker.droidify.data.encryption.Encrypted -import com.looker.droidify.data.encryption.Key -import com.looker.droidify.data.model.Authentication - -@Entity( - tableName = "authentication", - foreignKeys = [ - ForeignKey( - entity = RepoEntity::class, - childColumns = ["repoId"], - parentColumns = ["id"], - onDelete = CASCADE, - ), - ], -) -class AuthenticationEntity( - val password: Encrypted, - val username: String, - val initializationVector: ByteArray, - @PrimaryKey - val repoId: Int, -) - -fun AuthenticationEntity.toAuthentication(key: Key) = Authentication( - password = password.decrypt(key, initializationVector), - username = username, -) diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/AuthorEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/AuthorEntity.kt deleted file mode 100644 index 6455ea080..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/AuthorEntity.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.room.Entity -import androidx.room.Index -import androidx.room.PrimaryKey -import com.looker.droidify.data.model.Author -import com.looker.droidify.sync.v2.model.MetadataV2 - -@Entity( - tableName = "author", - indices = [Index("email", "name", "website", unique = true)], -) -data class AuthorEntity( - val email: String?, - val name: String?, - val website: String?, - @PrimaryKey(autoGenerate = true) - val id: Int = 0, -) - -fun MetadataV2.authorEntity() = AuthorEntity( - email = authorEmail, - name = authorName, - website = authorWebSite, -) - -fun AuthorEntity.toAuthor() = Author( - email = email, - name = name, - phone = null, - web = website, - id = id, -) diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/CategoryEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/CategoryEntity.kt deleted file mode 100644 index f8405bdcb..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/CategoryEntity.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Index -import com.looker.droidify.sync.v2.model.CategoryV2 -import com.looker.droidify.sync.v2.model.DefaultName - -@Entity( - tableName = "category", - primaryKeys = ["defaultName", "locale"], - indices = [Index("defaultName")], -) -data class CategoryEntity( - val icon: String?, - val name: String, - val description: String?, - val locale: String, - val defaultName: DefaultName, -) - -@Entity( - tableName = "category_repo_relation", - primaryKeys = ["id", "defaultName"], - foreignKeys = [ - ForeignKey( - entity = RepoEntity::class, - childColumns = ["id"], - parentColumns = ["id"], - onDelete = ForeignKey.CASCADE, - ), - ], -) -data class CategoryRepoRelation( - @ColumnInfo("id") - val repoId: Int, - val defaultName: DefaultName, -) - -@Entity( - tableName = "category_app_relation", - primaryKeys = ["id", "defaultName"], - indices = [Index("defaultName")], - foreignKeys = [ - ForeignKey( - entity = AppEntity::class, - childColumns = ["id"], - parentColumns = ["id"], - onDelete = ForeignKey.CASCADE, - ), - ], -) -data class CategoryAppRelation( - @ColumnInfo("id") - val appId: Int, - val defaultName: DefaultName, -) - -fun CategoryV2.categoryEntity( - defaultName: DefaultName, -): List { - return name.map { (locale, localizedName) -> - CategoryEntity( - icon = icon[locale]?.name, - name = localizedName, - description = description[locale], - defaultName = defaultName, - locale = locale, - ) - } -} - -// FIXME: This is a garbage algorithm -fun List.filterLocalized(locale: String): List = - filter { it.locale == locale }.ifEmpty { - filter { it.locale == "en-US" }.ifEmpty { - filter { it.locale == "en" } - } - } diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/DonateEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/DonateEntity.kt deleted file mode 100644 index 30df93645..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/DonateEntity.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.annotation.IntDef -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.Companion.CASCADE -import androidx.room.Index -import com.looker.droidify.data.model.Donation -import com.looker.droidify.sync.v2.model.MetadataV2 - -@Entity( - tableName = "donate", - primaryKeys = ["type", "appId"], - indices = [Index("appId")], - foreignKeys = [ - ForeignKey( - entity = AppEntity::class, - childColumns = ["appId"], - parentColumns = ["id"], - onDelete = CASCADE, - ), - ], -) -data class DonateEntity( - @param:DonationType - val type: Int, - val value: String, - val appId: Int, -) - -fun MetadataV2.donateEntity(appId: Int): List? { - return buildList { - if (bitcoin != null) { - add(DonateEntity(BITCOIN_ADD, bitcoin, appId)) - } - if (litecoin != null) { - add(DonateEntity(LITECOIN_ADD, litecoin, appId)) - } - if (liberapay != null) { - add(DonateEntity(LIBERAPAY_ID, liberapay, appId)) - } - if (openCollective != null) { - add(DonateEntity(OPEN_COLLECTIVE_ID, openCollective, appId)) - } - if (!donate.isNullOrEmpty()) { - add(DonateEntity(REGULAR, donate.joinToString(STRING_LIST_SEPARATOR), appId)) - } - }.ifEmpty { null } -} - -fun List.toDonation(): Donation { - var bitcoinAddress: String? = null - var litecoinAddress: String? = null - var liberapayId: String? = null - var openCollectiveId: String? = null - var flattrId: String? = null - var regular: List? = null - for (entity in this) { - when (entity.type) { - BITCOIN_ADD -> bitcoinAddress = entity.value - FLATTR_ID -> flattrId = entity.value - LIBERAPAY_ID -> liberapayId = entity.value - LITECOIN_ADD -> litecoinAddress = entity.value - OPEN_COLLECTIVE_ID -> openCollectiveId = entity.value - REGULAR -> regular = entity.value.split(STRING_LIST_SEPARATOR) - } - } - - return Donation( - bitcoinAddress = bitcoinAddress, - litecoinAddress = litecoinAddress, - liberapayId = liberapayId, - openCollectiveId = openCollectiveId, - flattrId = flattrId, - regularUrl = regular, - ) -} - -private const val STRING_LIST_SEPARATOR = "&^%#@!" - -@Retention(AnnotationRetention.BINARY) -@IntDef( - BITCOIN_ADD, - LITECOIN_ADD, - LIBERAPAY_ID, - OPEN_COLLECTIVE_ID, - FLATTR_ID, - REGULAR, -) -private annotation class DonationType - -private const val BITCOIN_ADD = 0 -private const val LITECOIN_ADD = 1 -private const val LIBERAPAY_ID = 2 -private const val OPEN_COLLECTIVE_ID = 3 -private const val FLATTR_ID = 4 -private const val REGULAR = 5 diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/GraphicEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/GraphicEntity.kt deleted file mode 100644 index a541457cc..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/GraphicEntity.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.annotation.IntDef -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.Companion.CASCADE -import androidx.room.Index -import com.looker.droidify.data.model.FilePath -import com.looker.droidify.data.model.Graphics -import com.looker.droidify.sync.v2.model.MetadataV2 - -@Entity( - tableName = "graphic", - primaryKeys = ["type", "locale", "appId"], - indices = [Index("appId", "locale"), Index("appId")], - foreignKeys = [ - ForeignKey( - entity = AppEntity::class, - childColumns = ["appId"], - parentColumns = ["id"], - onDelete = CASCADE, - ), - ], -) -data class GraphicEntity( - @param:GraphicType - val type: Int, - val url: String, - val locale: String, - val appId: Int, -) - -fun MetadataV2.localizedGraphics(appId: Int): List? { - return buildList { - promoGraphic?.forEach { (locale, value) -> - add(GraphicEntity(PROMO_GRAPHIC, value.name, locale, appId)) - } - featureGraphic?.forEach { (locale, value) -> - add(GraphicEntity(FEATURE_GRAPHIC, value.name, locale, appId)) - } - tvBanner?.forEach { (locale, value) -> - add(GraphicEntity(TV_BANNER, value.name, locale, appId)) - } - video?.forEach { (locale, value) -> - add(GraphicEntity(VIDEO, value, locale, appId)) - } - }.ifEmpty { null } -} - -fun List.toGraphics(locale: String, baseAddress: String): Graphics { - var featureGraphic: FilePath? = null - var promoGraphic: FilePath? = null - var tvBanner: FilePath? = null - var video: FilePath? = null - - for (entity in this) { - if (entity.locale != locale) continue - when (entity.type) { - FEATURE_GRAPHIC -> featureGraphic = FilePath(baseAddress, entity.url) - PROMO_GRAPHIC -> promoGraphic = FilePath(baseAddress, entity.url) - TV_BANNER -> tvBanner = FilePath(baseAddress, entity.url) - VIDEO -> video = FilePath(baseAddress, entity.url) - } - } - return Graphics( - featureGraphic = featureGraphic, - promoGraphic = promoGraphic, - tvBanner = tvBanner, - video = video, - ) -} - -@Retention(AnnotationRetention.BINARY) -@IntDef( - VIDEO, - TV_BANNER, - PROMO_GRAPHIC, - FEATURE_GRAPHIC, -) -annotation class GraphicType - -private const val VIDEO = 0 -private const val TV_BANNER = 1 -private const val PROMO_GRAPHIC = 2 -private const val FEATURE_GRAPHIC = 3 diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/InstalledEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/InstalledEntity.kt deleted file mode 100644 index 622ec64cf..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/InstalledEntity.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.looker.droidify.model.InstalledItem - -@Entity("installed") -data class InstalledEntity( - @PrimaryKey - val packageName: String, - val version: String, - val versionCode: Long, - val signature: String, -) - -/** - * Extension function to convert from domain model to entity - */ -fun InstalledItem.toEntity(): InstalledEntity = InstalledEntity( - packageName = packageName, - version = version, - versionCode = versionCode, - signature = signature, -) - -/** - * Extension function to convert from entity to domain model - */ -fun InstalledEntity.toDomain(): InstalledItem = InstalledItem( - packageName = packageName, - version = version, - versionCode = versionCode, - signature = signature, -) diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/LinksEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/LinksEntity.kt deleted file mode 100644 index edee1f95a..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/LinksEntity.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.Companion.CASCADE -import androidx.room.PrimaryKey -import com.looker.droidify.data.model.Links -import com.looker.droidify.sync.v2.model.MetadataV2 - -@Entity( - tableName = "link", - foreignKeys = [ - ForeignKey( - entity = AppEntity::class, - childColumns = ["appId"], - parentColumns = ["id"], - onDelete = CASCADE, - ), - ], -) -data class LinksEntity( - val changelog: String?, - val issueTracker: String?, - val translation: String?, - val sourceCode: String?, - val webSite: String?, - @PrimaryKey - val appId: Int, -) - -private fun MetadataV2.isLinkNull(): Boolean { - return changelog == null && - issueTracker == null && - translation == null && - sourceCode == null && - webSite == null -} - -fun MetadataV2.linkEntity(appId: Int) = if (!isLinkNull()) { - LinksEntity( - appId = appId, - changelog = changelog, - issueTracker = issueTracker, - translation = translation, - sourceCode = sourceCode, - webSite = webSite, - ) -} else { - null -} - -fun LinksEntity.toLinks() = Links( - changelog = changelog, - issueTracker = issueTracker, - translation = translation, - sourceCode = sourceCode, - webSite = webSite, -) diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/LocalizedAppEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/LocalizedAppEntity.kt deleted file mode 100644 index 006af08f8..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/LocalizedAppEntity.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.Companion.CASCADE -import androidx.room.Index -import com.looker.droidify.sync.v2.model.FileV2 - -@Entity( - tableName = "localized_app_name", - primaryKeys = ["appId", "locale"], - indices = [Index("appId"), Index("locale")], - foreignKeys = [ - ForeignKey( - entity = AppEntity::class, - parentColumns = ["id"], - childColumns = ["appId"], - onDelete = CASCADE, - ), - ], -) -data class LocalizedAppNameEntity( - override val appId: Int, - override val locale: String, - val name: String, -) : LocalizedEntityData - -@Entity( - tableName = "localized_app_summary", - primaryKeys = ["appId", "locale"], - indices = [Index("appId"), Index("locale")], - foreignKeys = [ - ForeignKey( - entity = AppEntity::class, - parentColumns = ["id"], - childColumns = ["appId"], - onDelete = CASCADE, - ), - ], -) -data class LocalizedAppSummaryEntity( - override val appId: Int, - override val locale: String, - val summary: String, -) : LocalizedEntityData - -@Entity( - tableName = "localized_app_description", - primaryKeys = ["appId", "locale"], - indices = [Index("appId"), Index("locale")], - foreignKeys = [ - ForeignKey( - entity = AppEntity::class, - parentColumns = ["id"], - childColumns = ["appId"], - onDelete = CASCADE, - ), - ], -) -data class LocalizedAppDescriptionEntity( - override val appId: Int, - override val locale: String, - val description: String, -) : LocalizedEntityData - -@Entity( - tableName = "localized_app_icon", - primaryKeys = ["appId", "locale"], - indices = [Index("appId"), Index("locale")], - foreignKeys = [ - ForeignKey( - entity = AppEntity::class, - parentColumns = ["id"], - childColumns = ["appId"], - onDelete = CASCADE, - ), - ], -) -data class LocalizedAppIconEntity( - override val appId: Int, - override val locale: String, - @Embedded(prefix = "icon_") - val icon: FileV2, -) : LocalizedEntityData - -sealed interface LocalizedEntityData { - val appId: Int - val locale: String -} - -fun List.findLocale(locale: String): T { - require(isNotEmpty()) { "List of localized data cannot be empty" } - var bestMatch: T? = null - var englishMatch: T? = null - for (i in indices) { - val match = get(i) - val l = match.locale - if (l == locale || l.startsWith(locale)) bestMatch = match - if (l == "en-US" || l == "en") englishMatch = match - } - return bestMatch ?: englishMatch ?: first() -} diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/LocalizedRepoEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/LocalizedRepoEntity.kt deleted file mode 100644 index e674c44a5..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/LocalizedRepoEntity.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.Companion.CASCADE -import androidx.room.Index -import com.looker.droidify.sync.v2.model.FileV2 - -@Entity( - tableName = "localized_repo_name", - primaryKeys = ["repoId", "locale"], - indices = [Index("repoId"), Index("locale")], - foreignKeys = [ - ForeignKey( - entity = RepoEntity::class, - parentColumns = ["id"], - childColumns = ["repoId"], - onDelete = CASCADE, - ), - ], -) -data class LocalizedRepoNameEntity( - val repoId: Int, - val locale: String, - val name: String, -) - -@Entity( - tableName = "localized_repo_description", - primaryKeys = ["repoId", "locale"], - indices = [Index("repoId"), Index("locale")], - foreignKeys = [ - ForeignKey( - entity = RepoEntity::class, - parentColumns = ["id"], - childColumns = ["repoId"], - onDelete = CASCADE, - ), - ], -) -data class LocalizedRepoDescriptionEntity( - val repoId: Int, - val locale: String, - val description: String, -) - -@Entity( - tableName = "localized_repo_icon", - primaryKeys = ["repoId", "locale"], - indices = [Index("repoId"), Index("locale")], - foreignKeys = [ - ForeignKey( - entity = RepoEntity::class, - parentColumns = ["id"], - childColumns = ["repoId"], - onDelete = CASCADE, - ), - ], -) -data class LocalizedRepoIconEntity( - val repoId: Int, - val locale: String, - @Embedded(prefix = "icon_") - val icon: FileV2, -) diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/MirrorEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/MirrorEntity.kt deleted file mode 100644 index 3b0a89ffe..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/MirrorEntity.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.Companion.CASCADE -import androidx.room.Index -import androidx.room.PrimaryKey -import com.looker.droidify.sync.v2.model.MirrorV2 - -@Entity( - tableName = "mirror", - indices = [Index("repoId")], - foreignKeys = [ - ForeignKey( - entity = RepoEntity::class, - childColumns = ["repoId"], - parentColumns = ["id"], - onDelete = CASCADE, - ), - ], -) -data class MirrorEntity( - val url: String, - val countryCode: String?, - val isPrimary: Boolean, - val repoId: Int, - @PrimaryKey(autoGenerate = true) - val id: Int = 0, -) - -fun MirrorV2.mirrorEntity(repoId: Int) = MirrorEntity( - url = url, - countryCode = countryCode, - isPrimary = isPrimary == true, - repoId = repoId, -) diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/RepoEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/RepoEntity.kt deleted file mode 100644 index 3f22ca5e9..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/RepoEntity.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.looker.droidify.data.model.Authentication -import com.looker.droidify.data.model.FilePath -import com.looker.droidify.data.model.Fingerprint -import com.looker.droidify.data.model.Html -import com.looker.droidify.data.model.Repo -import com.looker.droidify.data.model.VersionInfo -import com.looker.droidify.sync.v2.model.RepoV2 - -/** - * `enabled` flag will be kept in datastore and will be updated there only - * `deleted` is not needed as we will delete all required data when deleting repo or disabling it - * */ -@Entity(tableName = "repository") -data class RepoEntity( - val address: String, - val webBaseUrl: String?, - val fingerprint: Fingerprint, - val timestamp: Long?, - @PrimaryKey(autoGenerate = true) - val id: Int = 0, -) - -fun RepoV2.repoEntity( - id: Int, - fingerprint: Fingerprint, -) = RepoEntity( - id = id, - address = address, - timestamp = timestamp, - fingerprint = fingerprint, - webBaseUrl = webBaseUrl, -) - -fun RepoEntity.toRepo( - name: String, - description: String, - icon: String?, - mirrors: List, - enabled: Boolean, - authentication: Authentication?, -) = Repo( - icon = FilePath(address, icon), - name = name, - description = Html(description), - fingerprint = fingerprint, - authentication = authentication, - enabled = enabled, - address = address, - versionInfo = timestamp?.let { VersionInfo(timestamp = it, etag = null) }, - mirrors = mirrors, - id = id, -) diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/ScreenshotEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/ScreenshotEntity.kt deleted file mode 100644 index aa55b3254..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/ScreenshotEntity.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.annotation.IntDef -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.Companion.CASCADE -import androidx.room.Index -import com.looker.droidify.data.model.FilePath -import com.looker.droidify.data.model.Screenshots -import com.looker.droidify.sync.v2.model.LocalizedFiles -import com.looker.droidify.sync.v2.model.ScreenshotsV2 - -@Entity( - tableName = "screenshot", - primaryKeys = ["path", "type", "locale", "appId"], - indices = [Index("appId", "locale"), Index("appId")], - foreignKeys = [ - ForeignKey( - entity = AppEntity::class, - childColumns = ["appId"], - parentColumns = ["id"], - onDelete = CASCADE, - ), - ], -) -data class ScreenshotEntity( - val path: String, - @param:ScreenshotType - val type: Int, - val locale: String, - val appId: Int, -) - -fun ScreenshotsV2.localizedScreenshots(appId: Int): List { - if (isNull) return emptyList() - val screenshots = mutableListOf() - - val screenshotIterator: (Int, LocalizedFiles?) -> Unit = { type, localizedFiles -> - localizedFiles?.forEach { (locale, files) -> - for ((path, _, _) in files) { - screenshots.add( - ScreenshotEntity( - locale = locale, - appId = appId, - type = type, - path = path, - ), - ) - } - } - } - screenshotIterator(PHONE, phone) - screenshotIterator(SEVEN_INCH, sevenInch) - screenshotIterator(TEN_INCH, tenInch) - screenshotIterator(WEAR, wear) - screenshotIterator(TV, tv) - return screenshots -} - -fun List.toScreenshots(locale: String, baseAddress: String): Screenshots { - val phone = mutableListOf() - val sevenInch = mutableListOf() - val tenInch = mutableListOf() - val wear = mutableListOf() - val tv = mutableListOf() - - if (isEmpty()) return Screenshots() - - val requestedLang = locale.substringBefore('-') - val localesAvailable = map { it.locale }.toSet() - - val chosenLocale = when { - localesAvailable.contains(locale) -> locale - localesAvailable.any { it.substringBefore('-') == requestedLang } -> - localesAvailable.first { it.substringBefore('-') == requestedLang } - else -> localesAvailable.firstOrNull() - } - - if (chosenLocale == null) return Screenshots() - - for (entity in this) { - if (entity.locale != chosenLocale) continue - val path = FilePath(baseAddress, entity.path) - if (path != null) { - when (entity.type) { - PHONE -> phone.add(path) - SEVEN_INCH -> sevenInch.add(path) - TEN_INCH -> tenInch.add(path) - TV -> tv.add(path) - WEAR -> wear.add(path) - } - } - } - - return Screenshots( - phone = phone, - sevenInch = sevenInch, - tenInch = tenInch, - wear = wear, - tv = tv, - ) -} - -@Retention(AnnotationRetention.BINARY) -@IntDef( - PHONE, - SEVEN_INCH, - TEN_INCH, - WEAR, - TV, -) -private annotation class ScreenshotType - -private const val PHONE = 0 -private const val SEVEN_INCH = 1 -private const val TEN_INCH = 2 -private const val WEAR = 3 -private const val TV = 4 diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/VersionEntity.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/VersionEntity.kt deleted file mode 100644 index 9abbe75ac..000000000 --- a/app/src/main/kotlin/com/looker/droidify/data/local/model/VersionEntity.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.looker.droidify.data.local.model - -import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.ForeignKey.Companion.CASCADE -import androidx.room.Index -import androidx.room.PrimaryKey -import com.looker.droidify.data.model.ApkFile -import com.looker.droidify.data.model.Manifest -import com.looker.droidify.data.model.Package -import com.looker.droidify.data.model.Permission -import com.looker.droidify.data.model.Platforms -import com.looker.droidify.data.model.SDKs -import com.looker.droidify.network.DataSize -import com.looker.droidify.sync.v2.model.ApkFileV2 -import com.looker.droidify.sync.v2.model.FileV2 -import com.looker.droidify.sync.v2.model.LocalizedString -import com.looker.droidify.sync.v2.model.PackageV2 -import com.looker.droidify.sync.v2.model.PermissionV2 -import com.looker.droidify.sync.v2.model.localizedValue - -@Entity( - tableName = "version", - indices = [ - Index("appId"), - Index(value = ["appId", "versionCode"], unique = true), - ], - foreignKeys = [ - ForeignKey( - entity = AppEntity::class, - childColumns = ["appId"], - parentColumns = ["id"], - onDelete = CASCADE, - ), - ], -) -data class VersionEntity( - val added: Long, - val whatsNew: LocalizedString, - val versionName: String, - val versionCode: Long, - val maxSdkVersion: Int?, - val minSdkVersion: Int, - val targetSdkVersion: Int, - @Embedded("apk_") - val apk: ApkFileV2, - @Embedded("src_") - val src: FileV2?, - val features: List, - val nativeCode: List, - val permissions: List, - val permissionsSdk23: List, - val appId: Int, - @PrimaryKey(autoGenerate = true) - val id: Int = 0, -) - -fun PackageV2.versionEntities(appId: Int): Map> { - return versions.map { (_, version) -> - VersionEntity( - added = version.added, - whatsNew = version.whatsNew, - versionName = version.manifest.versionName, - versionCode = version.manifest.versionCode, - maxSdkVersion = version.manifest.maxSdkVersion, - minSdkVersion = version.manifest.usesSdk?.minSdkVersion ?: -1, - targetSdkVersion = version.manifest.usesSdk?.targetSdkVersion ?: -1, - apk = version.file, - src = version.src, - features = version.manifest.features.map { it.name }, - nativeCode = version.manifest.nativecode, - permissions = version.manifest.usesPermission, - permissionsSdk23 = version.manifest.usesPermissionSdk23, - appId = appId, - ) to version.antiFeatures.map { (tag, reason) -> - AntiFeatureAppRelation( - tag = tag, - reason = reason, - appId = appId, - versionCode = version.manifest.versionCode, - ) - } - }.toMap() -} - -fun List.toPackages( - locale: String, - installed: InstalledEntity?, -) = map { version -> - Package( - id = version.id.toLong(), - installed = installed != null && installed.versionCode == version.versionCode, - added = version.added, - apk = ApkFile( - name = version.apk.name, - hash = version.apk.sha256, - size = DataSize(version.apk.size), - ), - platforms = Platforms(version.nativeCode), - features = version.features, - antiFeatures = emptyList(), // This would need to be populated from AntiFeatureAppRelation - manifest = Manifest( - versionCode = version.versionCode, - versionName = version.versionName, - usesSDKs = SDKs( - min = version.minSdkVersion, - max = version.maxSdkVersion ?: -1, - target = version.targetSdkVersion, - ), - signer = emptySet(), // This would need to be populated from somewhere - permissions = version.permissions.map { - Permission( - name = it.name, - sdKs = SDKs( - min = -1, // PermissionV2 doesn't have minSdkVersion - max = it.maxSdkVersion ?: -1, - target = -1, - ), - ) - }, - ), - whatsNew = version.whatsNew.localizedValue(locale) ?: "", - ) -} diff --git a/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt b/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt index 1c9d85509..cf4fc640d 100644 --- a/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt +++ b/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt @@ -1,19 +1,10 @@ package com.looker.droidify.di -import android.content.Context import com.looker.droidify.data.PrivacyRepository -import com.looker.droidify.data.local.DroidifyDatabase -import com.looker.droidify.data.local.dao.AppDao -import com.looker.droidify.data.local.dao.AuthDao -import com.looker.droidify.data.local.dao.IndexDao -import com.looker.droidify.data.local.dao.InstalledDao -import com.looker.droidify.data.local.dao.RepoDao -import com.looker.droidify.data.local.droidifyDatabase import com.looker.droidify.datastore.SettingsRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @@ -21,43 +12,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object DatabaseModule { - @Singleton - @Provides - fun provideDatabase( - @ApplicationContext - context: Context, - ): DroidifyDatabase = droidifyDatabase(context) - - @Singleton - @Provides - fun provideAppDao( - db: DroidifyDatabase, - ): AppDao = db.appDao() - - @Singleton - @Provides - fun provideRepoDao( - db: DroidifyDatabase, - ): RepoDao = db.repoDao() - - @Singleton - @Provides - fun provideAuthDao( - db: DroidifyDatabase, - ): AuthDao = db.authDao() - - @Singleton - @Provides - fun provideInstallDao( - db: DroidifyDatabase, - ): InstalledDao = db.installedDao() - - @Singleton - @Provides - fun provideIndexDao( - db: DroidifyDatabase, - ): IndexDao = db.indexDao() - @Singleton @Provides fun providePrivacyRepository( diff --git a/app/src/main/kotlin/com/looker/droidify/di/RepoModule.kt b/app/src/main/kotlin/com/looker/droidify/di/RepoModule.kt deleted file mode 100644 index 6fd76de34..000000000 --- a/app/src/main/kotlin/com/looker/droidify/di/RepoModule.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.looker.droidify.di - -import android.content.Context -import com.looker.droidify.data.AppRepository -import com.looker.droidify.data.InstalledRepository -import com.looker.droidify.data.RepoRepository -import com.looker.droidify.data.encryption.EncryptionStorage -import com.looker.droidify.data.local.dao.AppDao -import com.looker.droidify.data.local.dao.AuthDao -import com.looker.droidify.data.local.dao.IndexDao -import com.looker.droidify.data.local.dao.InstalledDao -import com.looker.droidify.data.local.dao.RepoDao -import com.looker.droidify.datastore.SettingsRepository -import com.looker.droidify.network.Downloader -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineDispatcher - -@Module -@InstallIn(SingletonComponent::class) -object RepoModule { - - @Provides - fun provideRepoRepository( - repoDao: RepoDao, - appDao: AppDao, - authDao: AuthDao, - indexDao: IndexDao, - settingsRepository: SettingsRepository, - encryptionStorage: EncryptionStorage, - downloader: Downloader, - @ApplicationContext context: Context, - @IoDispatcher syncDispatcher: CoroutineDispatcher, - ): RepoRepository = RepoRepository( - encryptionStorage = encryptionStorage, - downloader = downloader, - context = context, - syncDispatcher = syncDispatcher, - repoDao = repoDao, - appDao = appDao, - authDao = authDao, - indexDao = indexDao, - settingsRepository = settingsRepository, - ) - - @Provides - fun provideAppRepository( - appDao: AppDao, - repoDao: RepoDao, - settingsRepository: SettingsRepository, - ): AppRepository = AppRepository( - appDao = appDao, - repoDao = repoDao, - settingsRepository = settingsRepository, - ) - - @Provides - fun provideInstalledRepository( - installedDao: InstalledDao, - ): InstalledRepository = InstalledRepository( - installedDao = installedDao, - ) -} diff --git a/app/src/main/kotlin/com/looker/droidify/work/SyncWorker.kt b/app/src/main/kotlin/com/looker/droidify/work/SyncWorker.kt deleted file mode 100644 index 9c7b5f022..000000000 --- a/app/src/main/kotlin/com/looker/droidify/work/SyncWorker.kt +++ /dev/null @@ -1,169 +0,0 @@ -package com.looker.droidify.work - -import android.content.Context -import android.util.Log -import androidx.core.app.NotificationCompat -import androidx.hilt.work.HiltWorker -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.CoroutineWorker -import androidx.work.Data -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.ExistingWorkPolicy -import androidx.work.ForegroundInfo -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager -import androidx.work.WorkerParameters -import androidx.work.hasKeyWithValueOfType -import com.looker.droidify.R -import com.looker.droidify.data.RepoRepository -import com.looker.droidify.sync.SyncState -import com.looker.droidify.utility.common.createNotificationChannel -import com.looker.droidify.utility.common.extension.exceptCancellation -import com.looker.droidify.utility.common.toForegroundInfo -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.util.concurrent.TimeUnit -import kotlin.time.Duration -import kotlin.time.toJavaDuration - -@HiltWorker -class SyncWorker @AssistedInject constructor( - @Assisted context: Context, - @Assisted workerParams: WorkerParameters, - private val repoRepository: RepoRepository, -) : CoroutineWorker(context, workerParams) { - override suspend fun doWork(): Result = withContext(Dispatchers.IO) { - val repoId = if (inputData.hasKeyWithValueOfType(KEY_REPO_ID)) { - inputData.getInt(KEY_REPO_ID, -1).takeIf { it >= 0 } - } else { - null - } - Log.i(TAG, "SyncWorker started (repoId=$repoId)") - try { - val success = if (repoId != null) { - val repo = repoRepository.getRepo(repoId) - if (repo != null) { - setForeground(createForegroundInfo(repo.name, -1)) - repoRepository.sync(repo) { state -> - val progress = - if (state is SyncState.IndexDownload.Progress) state.progress else -1 - setForegroundAsync(createForegroundInfo(repo.name, progress)) - } - } else { - Log.w(TAG, "Repo not found for id=$repoId; falling back to syncAll") - repoRepository.syncAll() - } - } else { - repoRepository.syncAll() - } - if (success) { - Log.i(TAG, "Sync completed successfully (repoId=$repoId)") - Result.success() - } else { - Log.w(TAG, "Sync reported failure (repoId=$repoId)") - Result.retry() - } - } catch (t: Throwable) { - t.exceptCancellation() - Log.e(TAG, "Sync failed with exception", t) - Result.retry() - } - } - - private fun createForegroundInfo(name: String, percent: Int): ForegroundInfo { - val id = "sync_channel" - val title = "Syncing: $name" - val cancel = applicationContext.getString(R.string.cancel) - val intent = WorkManager - .getInstance(applicationContext) - .createCancelPendingIntent(getId()) - - applicationContext.createNotificationChannel( - id = id, - name = "Sync channel", - showBadge = true, - ) - - val notification = NotificationCompat.Builder(applicationContext, id) - .setContentTitle(title) - .setTicker(title) - .setProgress(100, percent, percent == -1) - .setSmallIcon(R.drawable.ic_sync) - .setOngoing(true) - .addAction(R.drawable.ic_cancel, cancel, intent) - .build() - - return notification.toForegroundInfo(124) - } - - companion object { - private const val TAG = "SyncWorker" - private const val KEY_REPO_ID = "repo_id" - private const val KEY_TRIGGER = "trigger" - private const val TRIGGER_USER = "user" - private const val TRIGGER_PERIODIC = "periodic" - - private val defaultConstraints: Constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - - fun enqueueUserSync(context: Context, repoId: Int? = null) { - val data = Data.Builder() - .putString(KEY_TRIGGER, TRIGGER_USER) - .apply { if (repoId != null) putInt(KEY_REPO_ID, repoId) } - .build() - - val request = OneTimeWorkRequestBuilder() - .setInputData(data) - .setConstraints(defaultConstraints) - .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.SECONDS) - .addTag(TAG) - .build() - - WorkManager - .getInstance(context) - .enqueueUniqueWork( - uniqueWorkName = "$TAG.user", - existingWorkPolicy = ExistingWorkPolicy.KEEP, - request = request, - ) - Log.i(TAG, "User sync enqueued (repoId=$repoId)") - } - - fun syncRepo(context: Context, repoId: Int) { - enqueueUserSync(context, repoId) - } - - fun schedulePeriodicSync(context: Context, repeatInterval: Duration) { - val data = Data.Builder() - .putString(KEY_TRIGGER, TRIGGER_PERIODIC) - .build() - - val request = PeriodicWorkRequestBuilder(repeatInterval.toJavaDuration()) - .setInputData(data) - .setConstraints(defaultConstraints) - .addTag(TAG) - .build() - - WorkManager - .getInstance(context) - .enqueueUniquePeriodicWork( - uniqueWorkName = TAG, - existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.UPDATE, - request = request, - ) - Log.i(TAG, "Periodic sync scheduled every $repeatInterval") - } - - fun cancelAll(context: Context) { - WorkManager.getInstance(context).cancelUniqueWork(TAG) - WorkManager.getInstance(context).cancelAllWorkByTag(TAG) - Log.i(TAG, "All sync work cancelled") - } - } -} diff --git a/app/src/test/kotlin/com/looker/droidify/data/InstalledRepositoryTest.kt b/app/src/test/kotlin/com/looker/droidify/data/InstalledRepositoryTest.kt deleted file mode 100644 index a0dd1526b..000000000 --- a/app/src/test/kotlin/com/looker/droidify/data/InstalledRepositoryTest.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.looker.droidify.data - -import app.cash.turbine.test -import com.looker.droidify.data.local.dao.InstalledDao -import com.looker.droidify.data.local.model.InstalledEntity -import com.looker.droidify.model.InstalledItem -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class InstalledRepositoryTest { - - private lateinit var installedDao: InstalledDao - private lateinit var repository: InstalledRepository - - @BeforeEach - fun setup() { - installedDao = mockk(relaxed = true) - repository = InstalledRepository(installedDao) - } - - private fun testEntity( - packageName: String = "com.example.app", - version: String = "1.0.0", - versionCode: Long = 100, - signature: String = "abc123", - ) = InstalledEntity( - packageName = packageName, - version = version, - versionCode = versionCode, - signature = signature, - ) - - @Test - fun `get returns domain model when entity exists`() = runTest { - coEvery { installedDao.get("com.example.app") } returns testEntity() - - val result = repository.get("com.example.app") - - assertNotNull(result) - assertEquals("com.example.app", result!!.packageName) - assertEquals("1.0.0", result.version) - assertEquals(100L, result.versionCode) - } - - @Test - fun `get returns null when entity does not exist`() = runTest { - coEvery { installedDao.get("com.example.nonexistent") } returns null - - val result = repository.get("com.example.nonexistent") - assertNull(result) - } - - @Test - fun `getStream emits mapped domain model`() = runTest { - coEvery { installedDao.stream("com.example.app") } returns flowOf(testEntity()) - - repository.getStream("com.example.app").test { - val item = awaitItem() - assertNotNull(item) - assertEquals("com.example.app", item!!.packageName) - awaitComplete() - } - } - - @Test - fun `getStream emits null when entity not found`() = runTest { - coEvery { installedDao.stream("com.example.missing") } returns flowOf(null) - - repository.getStream("com.example.missing").test { - assertNull(awaitItem()) - awaitComplete() - } - } - - @Test - fun `getAllStream emits mapped list`() = runTest { - val entities = listOf( - testEntity(packageName = "com.app.one"), - testEntity(packageName = "com.app.two"), - ) - coEvery { installedDao.streamAll() } returns flowOf(entities) - - repository.getAllStream().test { - val items = awaitItem() - assertEquals(2, items.size) - assertEquals("com.app.one", items[0].packageName) - assertEquals("com.app.two", items[1].packageName) - awaitComplete() - } - } - - @Test - fun `put delegates to dao insert`() = runTest { - val item = InstalledItem( - packageName = "com.example.app", - version = "2.0.0", - versionCode = 200, - signature = "def456", - ) - - repository.put(item) - - coVerify { - installedDao.insert( - match { - it.packageName == "com.example.app" && - it.version == "2.0.0" && - it.versionCode == 200L - }, - ) - } - } - - @Test - fun `putAll delegates to dao replaceAll`() = runTest { - val items = listOf( - InstalledItem("com.app.one", "1.0", 1, "sig1"), - InstalledItem("com.app.two", "2.0", 2, "sig2"), - ) - - repository.putAll(items) - - coVerify { - installedDao.replaceAll(match { it.size == 2 }) - } - } - - @Test - fun `delete delegates to dao and returns count`() = runTest { - coEvery { installedDao.delete("com.example.app") } returns 1 - - val result = repository.delete("com.example.app") - - assertEquals(1, result) - coVerify { installedDao.delete("com.example.app") } - } -} diff --git a/app/src/test/kotlin/com/looker/droidify/data/local/BaseDatabaseTest.kt b/app/src/test/kotlin/com/looker/droidify/data/local/BaseDatabaseTest.kt deleted file mode 100644 index 4441a00de..000000000 --- a/app/src/test/kotlin/com/looker/droidify/data/local/BaseDatabaseTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.looker.droidify.data.local - -import android.content.Context -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.room.Room -import androidx.test.core.app.ApplicationProvider -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.rules.TestRule -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -/** - * Base class for Room database tests. - * Sets up an in-memory database for testing using Robolectric. - */ -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE) -abstract class BaseDatabaseTest { - - @get:Rule - val instantTaskExecutorRule: TestRule = InstantTaskExecutorRule() - protected lateinit var database: DroidifyDatabase - protected val testDispatcher: TestDispatcher = StandardTestDispatcher() - - @Before - fun setupDatabase() { - Dispatchers.setMain(testDispatcher) - val context = ApplicationProvider.getApplicationContext() - database = Room.inMemoryDatabaseBuilder( - context, - DroidifyDatabase::class.java, - ) - .allowMainThreadQueries() - .build() - - initDao() - } - - /** - * Hook method for subclasses to initialize their DAOs. - * This is called after the database is created. - */ - protected open fun initDao() { - // No-op by default, subclasses can override - } - - @After - fun tearDown() { - database.close() - Dispatchers.resetMain() - } - - /** - * Helper function to run a test with the test dispatcher - */ - fun runDbTest(block: suspend () -> Unit) = runTest(testDispatcher) { - block() - } -} diff --git a/app/src/test/kotlin/com/looker/droidify/data/local/dao/InstalledDaoTest.kt b/app/src/test/kotlin/com/looker/droidify/data/local/dao/InstalledDaoTest.kt deleted file mode 100644 index 5c46acc6e..000000000 --- a/app/src/test/kotlin/com/looker/droidify/data/local/dao/InstalledDaoTest.kt +++ /dev/null @@ -1,193 +0,0 @@ -package com.looker.droidify.data.local.dao - -import com.looker.droidify.data.local.BaseDatabaseTest -import com.looker.droidify.data.local.model.InstalledEntity -import kotlinx.coroutines.flow.first -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Test - -class InstalledDaoTest : BaseDatabaseTest() { - - private lateinit var installedDao: InstalledDao - - override fun initDao() { - installedDao = database.installedDao() - } - - @Test - fun insertAndGetInstalledEntity() = runDbTest { - // Given - val entity = InstalledEntity( - packageName = "com.example.app", - version = "1.0.0", - versionCode = 100, - signature = "abcdef123456", - ) - - // When - installedDao.insert(entity) - val result = installedDao.get("com.example.app") - - // Then - assertEquals(entity, result) - } - - @Test - fun getReturnsNullWhenPackageNotFound() = runDbTest { - // When - val result = installedDao.get("com.example.nonexistent") - - // Then - assertNull(result) - } - - @Test - fun streamReturnsFlowOfInstalledEntity() = runDbTest { - // Given - val entity = InstalledEntity( - packageName = "com.example.app", - version = "1.0.0", - versionCode = 100, - signature = "abcdef123456", - ) - installedDao.insert(entity) - - // When - val result = installedDao.stream("com.example.app").first() - - // Then - assertEquals(entity, result) - } - - @Test - fun streamAllReturnsFlowOfAllInstalledEntities() = runDbTest { - // Given - val entity1 = InstalledEntity( - packageName = "com.example.app1", - version = "1.0.0", - versionCode = 100, - signature = "abcdef123456", - ) - val entity2 = InstalledEntity( - packageName = "com.example.app2", - version = "2.0.0", - versionCode = 200, - signature = "ghijkl789012", - ) - installedDao.insertAll(listOf(entity1, entity2)) - - // When - val result = installedDao.streamAll().first() - - // Then - assertEquals(2, result.size) - assertEquals(listOf(entity1, entity2), result) - } - - @Test - fun insertAllInsertsMultipleEntities() = runDbTest { - // Given - val entity1 = InstalledEntity( - packageName = "com.example.app1", - version = "1.0.0", - versionCode = 100, - signature = "abcdef123456", - ) - val entity2 = InstalledEntity( - packageName = "com.example.app2", - version = "2.0.0", - versionCode = 200, - signature = "ghijkl789012", - ) - - // When - installedDao.insertAll(listOf(entity1, entity2)) - val result1 = installedDao.get("com.example.app1") - val result2 = installedDao.get("com.example.app2") - - // Then - assertEquals(entity1, result1) - assertEquals(entity2, result2) - } - - @Test - fun replaceAllReplacesAllEntities() = runDbTest { - // Given - val entity1 = InstalledEntity( - packageName = "com.example.app1", - version = "1.0.0", - versionCode = 100, - signature = "abcdef123456", - ) - val entity2 = InstalledEntity( - packageName = "com.example.app2", - version = "2.0.0", - versionCode = 200, - signature = "ghijkl789012", - ) - installedDao.insertAll(listOf(entity1, entity2)) - - val entity3 = InstalledEntity( - packageName = "com.example.app3", - version = "3.0.0", - versionCode = 300, - signature = "mnopqr345678", - ) - - // When - installedDao.replaceAll(listOf(entity3)) - val result = installedDao.streamAll().first() - - // Then - assertEquals(1, result.size) - assertEquals(entity3, result[0]) - assertNull(installedDao.get("com.example.app1")) - assertNull(installedDao.get("com.example.app2")) - } - - @Test - fun deleteRemovesEntity() = runDbTest { - // Given - val entity = InstalledEntity( - packageName = "com.example.app", - version = "1.0.0", - versionCode = 100, - signature = "abcdef123456", - ) - installedDao.insert(entity) - - // When - val deleteCount = installedDao.delete("com.example.app") - val result = installedDao.get("com.example.app") - - // Then - assertEquals(1, deleteCount) - assertNull(result) - } - - @Test - fun deleteAllRemovesAllEntities() = runDbTest { - // Given - val entity1 = InstalledEntity( - packageName = "com.example.app1", - version = "1.0.0", - versionCode = 100, - signature = "abcdef123456", - ) - val entity2 = InstalledEntity( - packageName = "com.example.app2", - version = "2.0.0", - versionCode = 200, - signature = "ghijkl789012", - ) - installedDao.insertAll(listOf(entity1, entity2)) - - // When - installedDao.deleteAll() - val result = installedDao.streamAll().first() - - // Then - assertEquals(0, result.size) - } -} diff --git a/app/src/test/kotlin/com/looker/droidify/data/local/dao/RepoDaoTest.kt b/app/src/test/kotlin/com/looker/droidify/data/local/dao/RepoDaoTest.kt deleted file mode 100644 index 1fd288d35..000000000 --- a/app/src/test/kotlin/com/looker/droidify/data/local/dao/RepoDaoTest.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.looker.droidify.data.local.dao - -import com.looker.droidify.data.local.BaseDatabaseTest -import com.looker.droidify.data.local.model.RepoEntity -import com.looker.droidify.data.model.Fingerprint -import kotlinx.coroutines.flow.first -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertNull -import org.junit.Test - -class RepoDaoTest : BaseDatabaseTest() { - - private lateinit var repoDao: RepoDao - private lateinit var indexDao: IndexDao - - override fun initDao() { - repoDao = database.repoDao() - indexDao = database.indexDao() - } - - private suspend fun insertTestRepo( - address: String = "https://f-droid.org/repo", - fingerprint: String = "ABC123", - timestamp: Long? = 1000L, - ): Int { - val entity = RepoEntity( - address = address, - webBaseUrl = null, - fingerprint = Fingerprint(fingerprint), - timestamp = timestamp, - ) - return indexDao.upsertRepo(entity) - } - - @Test - fun insertAndGetRepo() = runDbTest { - val id = insertTestRepo() - val result = repoDao.getRepo(id) - - assertNotNull(result) - assertEquals("https://f-droid.org/repo", result!!.address) - assertEquals(Fingerprint("ABC123"), result.fingerprint) - } - - @Test - fun getRepoReturnsNullWhenNotFound() = runDbTest { - val result = repoDao.getRepo(999) - assertNull(result) - } - - @Test - fun streamReturnsAllRepos() = runDbTest { - insertTestRepo(address = "https://repo1.org/repo", fingerprint = "FP1") - insertTestRepo(address = "https://repo2.org/repo", fingerprint = "FP2") - - val repos = repoDao.stream().first() - assertEquals(2, repos.size) - } - - @Test - fun repoFlowEmitsUpdatedRepo() = runDbTest { - val id = insertTestRepo() - val repo = repoDao.repo(id).first() - - assertNotNull(repo) - assertEquals("https://f-droid.org/repo", repo!!.address) - } - - @Test - fun deleteRemovesRepo() = runDbTest { - val id = insertTestRepo() - assertNotNull(repoDao.getRepo(id)) - - repoDao.delete(id) - assertNull(repoDao.getRepo(id)) - } - - @Test - fun resetTimestampSetsTimestampToNull() = runDbTest { - val id = insertTestRepo(timestamp = 5000L) - val before = repoDao.getRepo(id) - assertEquals(5000L, before!!.timestamp) - - repoDao.resetTimestamp(id) - val after = repoDao.getRepo(id) - assertNull(after!!.timestamp) - } - - @Test - fun getAddressByIdsReturnsCorrectMapping() = runDbTest { - val id1 = insertTestRepo(address = "https://repo1.org", fingerprint = "FP1") - val id2 = insertTestRepo(address = "https://repo2.org", fingerprint = "FP2") - - val result = repoDao.getAddressByIds(listOf(id1, id2)) - assertEquals(2, result.size) - assertEquals("https://repo1.org", result[id1]) - assertEquals("https://repo2.org", result[id2]) - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b6b829d5b..fc248f5cc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,6 @@ serialization = "1.10.0" ksp = "2.3.6" okhttp = "5.1.0" libsu = "6.0.0" -room = "2.8.4" shizuku = "13.0.0" sqldelight = "2.3.2" image-viewer = "1.0.1" @@ -78,10 +77,6 @@ serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp-mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver3", version.ref = "okhttp" } libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } -room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } -room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } -room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } -room-test = { group = "androidx.room", name = "room-testing", version.ref = "room" } sqldelight-android-driver = { group = "app.cash.sqldelight", name = "android-driver", version.ref = "sqldelight" } sqldelight-dialect-sqlite318 = { group = "app.cash.sqldelight", name = "sqlite-3-18-dialect", version.ref = "sqldelight" } sqldelight-coroutines = { group = "app.cash.sqldelight", name = "coroutines-extensions", version.ref = "sqldelight" } @@ -122,7 +117,6 @@ sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } [bundles] -room = ["room-runtime", "room-ktx"] sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-primitive-adapters"] shizuku = ["shizuku-provider", "shizuku-api"] coroutines = ["coroutines-core", "coroutines-android", "coroutines-guava"] From 02caea9ca2c45c5b1f9ffdb60b705cc892a7e6d9 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 18 Jul 2026 17:04:17 +0530 Subject: [PATCH 11/19] refactor: Add enabled and etag to repo table Might revert this later Signed-off-by: LooKeR --- .../droidify/data/local/sql/Repository.sq | 20 +++++++++++++++--- app/src/main/sqldelight/databases/1.db | Bin 143360 -> 143360 bytes 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq index 13bc181b0..c31d0bce9 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Repository.sq @@ -5,8 +5,10 @@ import kotlin.Boolean; CREATE TABLE repository ( address TEXT NOT NULL, webBaseUrl TEXT, - fingerprint BLOB AS Fingerprint NOT NULL, + fingerprint BLOB AS Fingerprint, + etag TEXT, timestamp INTEGER, + enabled INTEGER AS Boolean NOT NULL DEFAULT 1, id INTEGER NOT NULL PRIMARY KEY ); @@ -46,8 +48,20 @@ CREATE TABLE authentication ( ); insertRepo: -INSERT INTO repository(address, webBaseUrl, fingerprint, timestamp) -VALUES (?, ?, ?, ?); +INSERT INTO repository(address, webBaseUrl, fingerprint, etag, timestamp, enabled) +VALUES (?, ?, ?, ?, ?, ?); + +updateRepoEnabled: +UPDATE repository SET enabled = ? WHERE id = ?; + +updateRepoVersionInfo: +UPDATE repository SET fingerprint = ?, etag = ?, timestamp = ? WHERE id = ?; + +deleteRepo: +DELETE FROM repository WHERE id = ?; + +deleteMirrors: +DELETE FROM mirror WHERE repoId = ?; lastInsertRowId: SELECT last_insert_rowid(); diff --git a/app/src/main/sqldelight/databases/1.db b/app/src/main/sqldelight/databases/1.db index dc07c606cf0e47e408714e299ee4c12df516acac..89406969e2664d255ce3c728235f83b645a94038 100644 GIT binary patch delta 136 zcmZp8z|jCiTNs%XbPg~GGZZs$cXLhTT+gwWeLvd?md9+(%^q83l$i{*f>js*~JwV8Jmr#sWVpa>u@OmL25~2xtb%MCS!9{zn5HqwFrH_uVff9k zkUxs=8DIWnL4o4!#R`mJY@BYw?Ba@wj7{OwFR3wBa4PuuhbZ`k`uI$bRA-c({E=O; OeZM;6_WkNijvN3KM<5IU From 9646ac0d091a9e3cbe3006f8d03eca221e65e06d Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 18 Jul 2026 17:07:08 +0530 Subject: [PATCH 12/19] build: Add SQLDelight test deps Signed-off-by: LooKeR --- app/build.gradle.kts | 1 + gradle.properties | 1 + gradle/libs.versions.toml | 1 + 3 files changed, 3 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0b94a9f12..afb539770 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -170,6 +170,7 @@ dependencies { testImplementation(libs.test.core) testImplementation(libs.test.core.ktx) testImplementation(libs.mockk) + testImplementation(libs.sqldelight.sqlite.driver) testImplementation(libs.turbine) testImplementation(libs.hilt.test) testRuntimeOnly(libs.junit.platform) diff --git a/gradle.properties b/gradle.properties index 57b961be6..6104066c9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,3 +9,4 @@ org.gradle.caching=true org.gradle.configuration-cache=true org.gradle.daemon=true org.gradle.jvmargs=-Xmx6g -Xms256m -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC +android.disallowKotlinSourceSets=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc248f5cc..6fced65c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -81,6 +81,7 @@ sqldelight-android-driver = { group = "app.cash.sqldelight", name = "android-dri sqldelight-dialect-sqlite318 = { group = "app.cash.sqldelight", name = "sqlite-3-18-dialect", version.ref = "sqldelight" } sqldelight-coroutines = { group = "app.cash.sqldelight", name = "coroutines-extensions", version.ref = "sqldelight" } sqldelight-primitive-adapters = { group = "app.cash.sqldelight", name = "primitive-adapters", version.ref = "sqldelight" } +sqldelight-sqlite-driver = { group = "app.cash.sqldelight", name = "sqlite-driver", version.ref = "sqldelight" } shizuku-api = { group = "dev.rikka.shizuku", name = "api", version.ref = "shizuku" } shizuku-provider = { group = "dev.rikka.shizuku", name = "provider", version.ref = "shizuku" } image-viewer = { module = "com.github.stfalcon-studio:StfalconImageViewer", version.ref = "image-viewer" } From 1402856d1ac07061b031c8cbf01120e0b9b0fac3 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 18 Jul 2026 17:10:04 +0530 Subject: [PATCH 13/19] feat: Add db driver Signed-off-by: LooKeR --- .../droidify/data/local/DroidifyDbFactory.kt | 58 +++++++++++++++++++ .../droidify/data/local/model/MediaType.kt | 40 +++++++++++++ .../com/looker/droidify/di/DatabaseModule.kt | 29 ++++++++++ 3 files changed, 127 insertions(+) create mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/DroidifyDbFactory.kt create mode 100644 app/src/main/kotlin/com/looker/droidify/data/local/model/MediaType.kt diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/DroidifyDbFactory.kt b/app/src/main/kotlin/com/looker/droidify/data/local/DroidifyDbFactory.kt new file mode 100644 index 000000000..81b19edce --- /dev/null +++ b/app/src/main/kotlin/com/looker/droidify/data/local/DroidifyDbFactory.kt @@ -0,0 +1,58 @@ +package com.looker.droidify.data.local + +import app.cash.sqldelight.ColumnAdapter +import app.cash.sqldelight.adapter.primitive.IntColumnAdapter +import app.cash.sqldelight.db.SqlDriver +import com.looker.droidify.data.encryption.Encrypted +import com.looker.droidify.data.local.sql.Anti_features_app_relation +import com.looker.droidify.data.local.sql.Authentication +import com.looker.droidify.data.local.sql.Donate +import com.looker.droidify.data.local.sql.DroidifyDb +import com.looker.droidify.data.local.sql.Graphic +import com.looker.droidify.data.local.sql.Permission +import com.looker.droidify.data.local.sql.Repository +import com.looker.droidify.data.local.sql.Screenshot +import com.looker.droidify.data.local.sql.Version +import com.looker.droidify.data.model.Fingerprint +import com.looker.droidify.sync.JsonParser +import com.looker.droidify.sync.v2.model.LocalizedString + +private val localizedStringAdapter = object : ColumnAdapter { + override fun decode(databaseValue: String): LocalizedString = + JsonParser.decodeFromString(databaseValue) + + override fun encode(value: LocalizedString): String = + JsonParser.encodeToString(value) +} + +@OptIn(ExperimentalStdlibApi::class) +private val fingerprintAdapter = object : ColumnAdapter { + override fun decode(databaseValue: ByteArray): Fingerprint = Fingerprint(databaseValue) + + override fun encode(value: Fingerprint): ByteArray = value.value.hexToByteArray() +} + +private val encryptedAdapter = object : ColumnAdapter { + override fun decode(databaseValue: String): Encrypted = Encrypted(databaseValue) + + override fun encode(value: Encrypted): String = value.value +} + +fun droidifyDb(driver: SqlDriver): DroidifyDb = DroidifyDb( + driver = driver, + anti_features_app_relationAdapter = Anti_features_app_relation.Adapter( + reasonAdapter = localizedStringAdapter, + ), + authenticationAdapter = Authentication.Adapter(passwordAdapter = encryptedAdapter), + donateAdapter = Donate.Adapter(typeAdapter = IntColumnAdapter), + graphicAdapter = Graphic.Adapter(typeAdapter = IntColumnAdapter), + permissionAdapter = Permission.Adapter(maxSdkVersionAdapter = IntColumnAdapter), + repositoryAdapter = Repository.Adapter(fingerprintAdapter = fingerprintAdapter), + screenshotAdapter = Screenshot.Adapter(typeAdapter = IntColumnAdapter), + versionAdapter = Version.Adapter( + whatsNewAdapter = localizedStringAdapter, + maxSdkVersionAdapter = IntColumnAdapter, + minSdkVersionAdapter = IntColumnAdapter, + targetSdkVersionAdapter = IntColumnAdapter, + ), +) diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/model/MediaType.kt b/app/src/main/kotlin/com/looker/droidify/data/local/model/MediaType.kt new file mode 100644 index 000000000..a44bb2780 --- /dev/null +++ b/app/src/main/kotlin/com/looker/droidify/data/local/model/MediaType.kt @@ -0,0 +1,40 @@ +package com.looker.droidify.data.local.model + +/** + * Stable integer discriminators for the `type` columns of the `graphic`, + * `screenshot` and `donate` tables. Never reorder — values are persisted. + */ +enum class GraphicType(val value: Int) { + FEATURE_GRAPHIC(0), + PROMO_GRAPHIC(1), + TV_BANNER(2), + VIDEO(3); + + companion object { + fun fromValue(value: Int): GraphicType = entries.first { it.value == value } + } +} + +enum class ScreenshotType(val value: Int) { + PHONE(0), + SEVEN_INCH(1), + TEN_INCH(2), + WEAR(3), + TV(4); + + companion object { + fun fromValue(value: Int): ScreenshotType = entries.first { it.value == value } + } +} + +enum class DonateType(val value: Int) { + REGULAR(0), + BITCOIN(1), + LITECOIN(2), + LIBERAPAY(3), + OPEN_COLLECTIVE(4); + + companion object { + fun fromValue(value: Int): DonateType = entries.first { it.value == value } + } +} diff --git a/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt b/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt index cf4fc640d..5b1b10c2d 100644 --- a/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt +++ b/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt @@ -1,17 +1,46 @@ package com.looker.droidify.di +import android.content.Context +import androidx.sqlite.db.SupportSQLiteDatabase +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.android.AndroidSqliteDriver import com.looker.droidify.data.PrivacyRepository +import com.looker.droidify.data.local.droidifyDb +import com.looker.droidify.data.local.sql.DroidifyDb import com.looker.droidify.datastore.SettingsRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +private const val DB_NAME = "droidify_v2.db" + @Module @InstallIn(SingletonComponent::class) object DatabaseModule { + @Singleton + @Provides + fun provideSqlDriver( + @ApplicationContext context: Context, + ): SqlDriver = AndroidSqliteDriver( + schema = DroidifyDb.Schema, + context = context, + name = DB_NAME, + callback = object : AndroidSqliteDriver.Callback(DroidifyDb.Schema) { + override fun onConfigure(db: SupportSQLiteDatabase) { + super.onConfigure(db) + db.setForeignKeyConstraintsEnabled(true) + } + }, + ) + + @Singleton + @Provides + fun provideDroidifyDb(driver: SqlDriver): DroidifyDb = droidifyDb(driver) + @Singleton @Provides fun providePrivacyRepository( From a56f55769fdb5948b13e68cb512778d1bc31f889 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sat, 18 Jul 2026 17:19:50 +0530 Subject: [PATCH 14/19] feat: Add repo repository Signed-off-by: LooKeR --- .../looker/droidify/data/RepoRepository.kt | 72 ++++++++ .../com/looker/droidify/di/DatabaseModule.kt | 15 ++ .../droidify/data/RepoRepositoryTest.kt | 158 ++++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 app/src/main/kotlin/com/looker/droidify/data/RepoRepository.kt create mode 100644 app/src/test/kotlin/com/looker/droidify/data/RepoRepositoryTest.kt diff --git a/app/src/main/kotlin/com/looker/droidify/data/RepoRepository.kt b/app/src/main/kotlin/com/looker/droidify/data/RepoRepository.kt new file mode 100644 index 000000000..48843773e --- /dev/null +++ b/app/src/main/kotlin/com/looker/droidify/data/RepoRepository.kt @@ -0,0 +1,72 @@ +package com.looker.droidify.data + +import com.looker.droidify.data.encryption.EncryptionStorage +import com.looker.droidify.data.local.sql.DroidifyDb +import com.looker.droidify.data.model.Fingerprint +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext + +class RepoRepository( + private val db: DroidifyDb, + private val encryptionStorage: EncryptionStorage, + private val dispatcher: CoroutineDispatcher, +) { + + suspend fun insert( + address: String, + fingerprint: Fingerprint? = null, + username: String? = null, + password: String? = null, + enabled: Boolean = true, + ): Long = withContext(dispatcher) { + val key = if (username != null && password != null) { + encryptionStorage.key.first() + } else { + null + } + db.transactionWithResult { + db.repositoryQueries.insertRepo( + address = address.removeSuffix("/"), + webBaseUrl = null, + fingerprint = fingerprint, + etag = null, + timestamp = null, + enabled = enabled, + ) + val repoId = db.repositoryQueries.lastInsertRowId().executeAsOne() + if (key != null && username != null && password != null) { + val (encrypted, iv) = key.encrypt(password) + db.repositoryQueries.insertAuthentication( + password = encrypted, + username = username, + initializationVector = iv, + repoId = repoId, + ) + } + repoId + } + } + + suspend fun setEnabled(repoId: Long, enabled: Boolean) = withContext(dispatcher) { + db.repositoryQueries.updateRepoEnabled(enabled, repoId) + } + + suspend fun updateVersionInfo( + repoId: Long, + fingerprint: Fingerprint?, + timestamp: Long?, + etag: String?, + ) = withContext(dispatcher) { + db.repositoryQueries.updateRepoVersionInfo( + fingerprint = fingerprint, + etag = etag, + timestamp = timestamp, + id = repoId, + ) + } + + suspend fun delete(repoId: Long) = withContext(dispatcher) { + db.repositoryQueries.deleteRepo(repoId) + } +} diff --git a/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt b/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt index 5b1b10c2d..861818314 100644 --- a/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt +++ b/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt @@ -5,6 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver import com.looker.droidify.data.PrivacyRepository +import com.looker.droidify.data.RepoRepository +import com.looker.droidify.data.encryption.EncryptionStorage import com.looker.droidify.data.local.droidifyDb import com.looker.droidify.data.local.sql.DroidifyDb import com.looker.droidify.datastore.SettingsRepository @@ -13,6 +15,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher import javax.inject.Singleton private const val DB_NAME = "droidify_v2.db" @@ -41,6 +44,18 @@ object DatabaseModule { @Provides fun provideDroidifyDb(driver: SqlDriver): DroidifyDb = droidifyDb(driver) + @Singleton + @Provides + fun provideRepoRepository( + db: DroidifyDb, + encryptionStorage: EncryptionStorage, + @IoDispatcher dispatcher: CoroutineDispatcher, + ): RepoRepository = RepoRepository( + db = db, + encryptionStorage = encryptionStorage, + dispatcher = dispatcher, + ) + @Singleton @Provides fun providePrivacyRepository( diff --git a/app/src/test/kotlin/com/looker/droidify/data/RepoRepositoryTest.kt b/app/src/test/kotlin/com/looker/droidify/data/RepoRepositoryTest.kt new file mode 100644 index 000000000..e6d8f86f4 --- /dev/null +++ b/app/src/test/kotlin/com/looker/droidify/data/RepoRepositoryTest.kt @@ -0,0 +1,158 @@ +package com.looker.droidify.data + +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import com.looker.droidify.data.encryption.Encrypted +import com.looker.droidify.data.encryption.EncryptionStorage +import com.looker.droidify.data.encryption.Key +import com.looker.droidify.data.local.droidifyDb +import com.looker.droidify.data.local.sql.DroidifyDb +import com.looker.droidify.data.model.Fingerprint +import io.mockk.every +import io.mockk.mockk +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest + +class RepoRepositoryTest { + + private lateinit var driver: SqlDriver + private lateinit var db: DroidifyDb + private lateinit var repoRepository: RepoRepository + + private val key = Key() + private val encryptionStorage = mockk { + every { key } returns flowOf(this@RepoRepositoryTest.key) + } + + @BeforeTest + fun setup() { + driver = JdbcSqliteDriver( + JdbcSqliteDriver.IN_MEMORY, + java.util.Properties().apply { put("foreign_keys", "true") }, + ) + DroidifyDb.Schema.create(driver) + db = droidifyDb(driver) + repoRepository = RepoRepository(db, encryptionStorage, Dispatchers.Unconfined) + } + + @AfterTest + fun teardown() { + driver.close() + } + + @Test + fun `insert creates enabled repository without auth`() = runTest { + val repoId = repoRepository.insert(address = "https://repo.test/repo/") + + assertEquals(1, count("repository", "id = $repoId AND enabled = 1")) + assertEquals(1, count("repository", "address = 'https://repo.test/repo'")) + assertEquals(0, count("authentication")) + } + + @Test + fun `insert with credentials stores decryptable authentication`() = runTest { + val repoId = repoRepository.insert( + address = "https://repo.test/repo", + username = "user", + password = "secret", + ) + + assertEquals(1, count("authentication", "repoId = $repoId AND username = 'user'")) + val (username, password, iv) = driver.executeQuery( + identifier = null, + sql = "SELECT username, password, initializationVector FROM authentication " + + "WHERE repoId = $repoId", + mapper = { cursor -> + cursor.next() + QueryResult.Value( + listOf( + cursor.getString(0), + cursor.getString(1), + cursor.getBytes(2), + ), + ) + }, + parameters = 0, + ).value + val decrypted = Encrypted(password as String).decrypt(key, iv as ByteArray) + assertEquals("secret", decrypted) + assertEquals("user", username) + } + + @Test + fun `setEnabled toggles flag`() = runTest { + val repoId = repoRepository.insert(address = "https://repo.test/repo") + + repoRepository.setEnabled(repoId, false) + + assertEquals(1, count("repository", "id = $repoId AND enabled = 0")) + } + + @Test + fun `updateVersionInfo stores sync metadata`() = runTest { + val repoId = repoRepository.insert(address = "https://repo.test/repo") + + repoRepository.updateVersionInfo( + repoId = repoId, + fingerprint = Fingerprint("ab".repeat(32)), + timestamp = 1720000000000, + etag = "etag-1", + ) + + assertEquals( + 1, + count( + "repository", + "id = $repoId AND timestamp = 1720000000000 AND etag = 'etag-1' " + + "AND fingerprint IS NOT NULL", + ), + ) + } + + @Test + fun `delete cascades to dependent rows`() = runTest { + val repoId = repoRepository.insert( + address = "https://repo.test/repo", + username = "user", + password = "secret", + ) + db.repositoryQueries.insertMirror( + url = "https://mirror.test/repo", + countryCode = null, + isPrimary = true, + repoId = repoId, + ) + + repoRepository.delete(repoId) + + assertEquals(0, count("repository")) + assertEquals(0, count("authentication")) + assertEquals(0, count("mirror")) + } + + private fun count(table: String, where: String? = null): Long { + val sql = buildString { + append("SELECT COUNT(*) FROM ") + append(table) + if (where != null) { + append(" WHERE ") + append(where) + } + } + return driver.executeQuery( + identifier = null, + sql = sql, + mapper = { cursor -> + cursor.next() + QueryResult.Value(requireNotNull(cursor.getLong(0))) + }, + parameters = 0, + ).value + } +} From 83ec5b2c42957e93c2bb8353f77003fa98f3f34b Mon Sep 17 00:00:00 2001 From: LooKeR Date: Sun, 19 Jul 2026 11:42:33 +0530 Subject: [PATCH 15/19] feat: Add index repository Signed-off-by: LooKeR --- .../looker/droidify/data/IndexRepository.kt | 270 ++++++++++++++++++ .../com/looker/droidify/di/DatabaseModule.kt | 11 + .../com/looker/droidify/data/local/sql/App.sq | 10 +- .../droidify/data/local/sql/AppMetadata.sq | 10 +- .../looker/droidify/data/local/sql/Version.sq | 10 +- .../droidify/data/IndexRepositoryBenchmark.kt | 120 ++++++++ .../droidify/data/IndexRepositoryTest.kt | 236 +++++++++++++++ 7 files changed, 658 insertions(+), 9 deletions(-) create mode 100644 app/src/main/kotlin/com/looker/droidify/data/IndexRepository.kt create mode 100644 app/src/test/kotlin/com/looker/droidify/data/IndexRepositoryBenchmark.kt create mode 100644 app/src/test/kotlin/com/looker/droidify/data/IndexRepositoryTest.kt diff --git a/app/src/main/kotlin/com/looker/droidify/data/IndexRepository.kt b/app/src/main/kotlin/com/looker/droidify/data/IndexRepository.kt new file mode 100644 index 000000000..66d92c218 --- /dev/null +++ b/app/src/main/kotlin/com/looker/droidify/data/IndexRepository.kt @@ -0,0 +1,270 @@ +package com.looker.droidify.data + +import com.looker.droidify.data.local.model.DonateType +import com.looker.droidify.data.local.model.GraphicType +import com.looker.droidify.data.local.model.ScreenshotType +import com.looker.droidify.data.local.sql.DroidifyDb +import com.looker.droidify.data.model.Fingerprint +import com.looker.droidify.sync.v2.model.CategoryV2 +import com.looker.droidify.sync.v2.model.AntiFeatureV2 +import com.looker.droidify.sync.v2.model.DefaultName +import com.looker.droidify.sync.v2.model.IndexV2 +import com.looker.droidify.sync.v2.model.LocalizedFiles +import com.looker.droidify.sync.v2.model.LocalizedIcon +import com.looker.droidify.sync.v2.model.MetadataV2 +import com.looker.droidify.sync.v2.model.PackageV2 +import com.looker.droidify.sync.v2.model.RepoV2 +import com.looker.droidify.sync.v2.model.Tag +import com.looker.droidify.sync.v2.model.VersionV2 +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +class IndexRepository( + private val db: DroidifyDb, + private val dispatcher: CoroutineDispatcher, +) { + + suspend fun insertIndex( + repoId: Long, + fingerprint: Fingerprint, + index: IndexV2, + etag: String? = null, + ) = withContext(dispatcher) { + db.transaction { + insertRepo(repoId, fingerprint, index.repo, etag) + index.packages.forEach { (packageName, packageV2) -> + insertPackage(repoId, packageName, packageV2) + } + } + } + + private fun insertRepo(repoId: Long, fingerprint: Fingerprint, repo: RepoV2, etag: String?) { + db.repositoryQueries.updateRepoVersionInfo( + fingerprint = fingerprint, + etag = etag, + timestamp = repo.timestamp, + id = repoId, + ) + + val locales = repo.name.keys + repo.description.keys + repo.icon?.keys.orEmpty() + locales.forEach { locale -> + val icon = repo.icon?.get(locale) + db.repositoryQueries.insertLocalizedRepo( + repoId = repoId, + locale = locale, + name = repo.name[locale], + description = repo.description[locale], + iconName = icon?.name, + iconSha256 = icon?.sha256?.hexOrNull(), + iconSize = icon?.size, + ) + } + + db.repositoryQueries.deleteMirrors(repoId) + repo.mirrors.forEach { mirror -> + db.repositoryQueries.insertMirror( + url = mirror.url, + countryCode = mirror.countryCode, + isPrimary = mirror.isPrimary == true, + repoId = repoId, + ) + } + + repo.categories.forEach { (defaultName, category) -> + insertCategory(repoId, defaultName, category) + } + repo.antiFeatures.forEach { (tag, antiFeature) -> + insertAntiFeature(repoId, tag, antiFeature) + } + } + + private fun insertCategory(repoId: Long, defaultName: DefaultName, category: CategoryV2) { + val locales = category.name.keys + category.description.keys + category.icon.keys + locales.forEach { locale -> + db.categoryQueries.insertCategory( + icon = category.icon[locale]?.name, + name = category.name[locale] ?: defaultName, + description = category.description[locale], + locale = locale, + defaultName = defaultName, + ) + } + db.categoryQueries.insertCategoryRepoRelation(repoId = repoId, defaultName = defaultName) + } + + private fun insertAntiFeature(repoId: Long, tag: Tag, antiFeature: AntiFeatureV2) { + val locales = antiFeature.name.keys + antiFeature.description.keys + antiFeature.icon.keys + locales.forEach { locale -> + db.antiFeatureQueries.insertAntiFeature( + icon = antiFeature.icon[locale]?.name, + name = antiFeature.name[locale] ?: tag, + description = antiFeature.description[locale], + locale = locale, + tag = tag, + ) + } + db.antiFeatureQueries.insertAntiFeatureRepoRelation(repoId = repoId, tag = tag) + } + + private fun insertPackage(repoId: Long, packageName: String, packageV2: PackageV2) { + val metadata = packageV2.metadata + + val authorEmail = metadata.authorEmail.orEmpty() + val authorName = metadata.authorName.orEmpty() + val authorWebSite = metadata.authorWebSite.orEmpty() + val authorId = db.appMetadataQueries.insertAuthor( + email = authorEmail, + name = authorName, + website = authorWebSite, + ).executeAsOne() + + val appId = db.appQueries.insertApp( + added = metadata.added, + lastUpdated = metadata.lastUpdated, + preferredSigner = metadata.preferredSigner?.hexOrNull(), + packageName = packageName, + authorId = authorId, + repoId = repoId, + ).executeAsOne() + + insertLocalizedApp(appId, metadata) + metadata.categories.forEach { defaultName -> + db.categoryQueries.insertCategoryAppRelation(appId = appId, defaultName = defaultName) + } + db.appMetadataQueries.insertLinks( + license = metadata.license, + changelog = metadata.changelog, + issueTracker = metadata.issueTracker, + translation = metadata.translation, + sourceCode = metadata.sourceCode, + webSite = metadata.webSite, + appId = appId, + ) + insertGraphics(appId, metadata) + insertScreenshots(appId, metadata) + insertDonations(appId, metadata) + + packageV2.versions.values.forEach { version -> insertVersion(appId, version) } + } + + private fun insertLocalizedApp(appId: Long, metadata: MetadataV2) { + val locales = metadata.name?.keys.orEmpty() + + metadata.summary?.keys.orEmpty() + + metadata.icon?.keys.orEmpty() + + metadata.description?.keys.orEmpty() + locales.forEach { locale -> + val icon = metadata.icon?.get(locale) + db.appQueries.insertLocalizedApp( + appId = appId, + locale = locale, + name = metadata.name?.get(locale), + summary = metadata.summary?.get(locale), + iconName = icon?.name, + iconSha256 = icon?.sha256?.hexOrNull(), + iconSize = icon?.size, + description = metadata.description?.get(locale), + ) + } + } + + private fun insertGraphics(appId: Long, metadata: MetadataV2) { + fun LocalizedIcon?.insert(type: GraphicType) = this?.forEach { (locale, file) -> + db.appMetadataQueries.insertGraphic( + url = file.name, + type = type.value, + locale = locale, + appId = appId, + ) + } + metadata.featureGraphic.insert(GraphicType.FEATURE_GRAPHIC) + metadata.promoGraphic.insert(GraphicType.PROMO_GRAPHIC) + metadata.tvBanner.insert(GraphicType.TV_BANNER) + metadata.video?.forEach { (locale, url) -> + db.appMetadataQueries.insertGraphic( + url = url, + type = GraphicType.VIDEO.value, + locale = locale, + appId = appId, + ) + } + } + + private fun insertScreenshots(appId: Long, metadata: MetadataV2) { + fun LocalizedFiles?.insert(type: ScreenshotType) = this?.forEach { (locale, files) -> + files.forEach { file -> + db.appMetadataQueries.insertScreenshot( + path = file.name, + type = type.value, + locale = locale, + appId = appId, + ) + } + } + val screenshots = metadata.screenshots ?: return + screenshots.phone.insert(ScreenshotType.PHONE) + screenshots.sevenInch.insert(ScreenshotType.SEVEN_INCH) + screenshots.tenInch.insert(ScreenshotType.TEN_INCH) + screenshots.wear.insert(ScreenshotType.WEAR) + screenshots.tv.insert(ScreenshotType.TV) + } + + private fun insertDonations(appId: Long, metadata: MetadataV2) { + fun String?.insert(type: DonateType) = this?.let { value -> + db.appMetadataQueries.insertDonate(type = type.value, value_ = value, appId = appId) + } + metadata.donate.forEach { url -> url.insert(DonateType.REGULAR) } + metadata.bitcoin.insert(DonateType.BITCOIN) + metadata.litecoin.insert(DonateType.LITECOIN) + metadata.liberapay.insert(DonateType.LIBERAPAY) + metadata.openCollective.insert(DonateType.OPEN_COLLECTIVE) + } + + private fun insertVersion(appId: Long, version: VersionV2) { + val manifest = version.manifest + val minSdkVersion = manifest.usesSdk?.minSdkVersion ?: 1 + val apkSha256 = requireNotNull(version.file.sha256.hexOrNull()) { + "Invalid apk sha256 for ${version.file.name}" + } + val versionId = db.versionQueries.insertVersion( + added = version.added, + whatsNew = version.whatsNew, + versionName = manifest.versionName, + versionCode = manifest.versionCode, + maxSdkVersion = manifest.maxSdkVersion, + minSdkVersion = minSdkVersion, + targetSdkVersion = manifest.usesSdk?.targetSdkVersion ?: minSdkVersion, + apkName = version.file.name, + apkSha256 = apkSha256, + apkSize = version.file.size, + appId = appId, + ).executeAsOne() + + (manifest.usesPermission + manifest.usesPermissionSdk23).forEach { permission -> + db.versionQueries.insertPermission( + name = permission.name, + maxSdkVersion = permission.maxSdkVersion, + versionId = versionId, + ) + } + manifest.features.forEach { feature -> + db.versionQueries.insertFeature(name = feature.name, versionId = versionId) + } + manifest.nativecode.forEach { abi -> + db.versionQueries.insertNativeCode(abi = abi, versionId = versionId) + } + version.antiFeatures.forEach { (tag, reason) -> + db.antiFeatureQueries.insertAntiFeatureAppRelation( + tag = tag, + reason = reason, + versionId = versionId, + ) + } + } +} + +@OptIn(ExperimentalStdlibApi::class) +private fun String.hexOrNull(): ByteArray? = try { + hexToByteArray() +} catch (_: IllegalArgumentException) { + null +} diff --git a/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt b/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt index 861818314..37744da29 100644 --- a/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt +++ b/app/src/main/kotlin/com/looker/droidify/di/DatabaseModule.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.sqlite.db.SupportSQLiteDatabase import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver +import com.looker.droidify.data.IndexRepository import com.looker.droidify.data.PrivacyRepository import com.looker.droidify.data.RepoRepository import com.looker.droidify.data.encryption.EncryptionStorage @@ -56,6 +57,16 @@ object DatabaseModule { dispatcher = dispatcher, ) + @Singleton + @Provides + fun provideIndexRepository( + db: DroidifyDb, + @IoDispatcher dispatcher: CoroutineDispatcher, + ): IndexRepository = IndexRepository( + db = db, + dispatcher = dispatcher, + ) + @Singleton @Provides fun providePrivacyRepository( diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq index 76a5f8e64..1a5d079dd 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/App.sq @@ -30,9 +30,13 @@ CREATE TABLE localized_app ( ) ); -insertApp: -INSERT OR IGNORE INTO app(added, lastUpdated, preferredSigner, packageName, authorId, repoId) -VALUES (?, ?, ?, ?, ?, ?); +insertApp { + INSERT OR IGNORE INTO app(added, lastUpdated, preferredSigner, packageName, authorId, repoId) + VALUES (:added, :lastUpdated, :preferredSigner, :packageName, :authorId, :repoId); + + SELECT id FROM app + WHERE packageName = :packageName AND repoId = :repoId; +} selectAppId: SELECT id FROM app diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq index 63acd2368..164ad769b 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppMetadata.sq @@ -42,9 +42,13 @@ CREATE TABLE donate ( PRIMARY KEY(appId, type, value) ) WITHOUT ROWID; -insertAuthor: -INSERT OR IGNORE INTO author(email, name, website) -VALUES (?, ?, ?); +insertAuthor { + INSERT OR IGNORE INTO author(email, name, website) + VALUES (:email, :name, :website); + + SELECT id FROM author + WHERE email = :email AND name = :name AND website = :website; +} selectAuthorId: SELECT id FROM author diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq index 2c6a23b05..191e82198 100644 --- a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Version.sq @@ -38,9 +38,13 @@ CREATE TABLE native_code ( PRIMARY KEY(versionId, abi) ) WITHOUT ROWID; -insertVersion: -INSERT OR IGNORE INTO version(added, whatsNew, versionName, versionCode, maxSdkVersion, minSdkVersion, targetSdkVersion, apkName, apkSha256, apkSize, appId) -VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); +insertVersion { + INSERT OR IGNORE INTO version(added, whatsNew, versionName, versionCode, maxSdkVersion, minSdkVersion, targetSdkVersion, apkName, apkSha256, apkSize, appId) + VALUES (:added, :whatsNew, :versionName, :versionCode, :maxSdkVersion, :minSdkVersion, :targetSdkVersion, :apkName, :apkSha256, :apkSize, :appId); + + SELECT id FROM version + WHERE appId = :appId AND apkSha256 = :apkSha256; +} selectVersionId: SELECT id FROM version diff --git a/app/src/test/kotlin/com/looker/droidify/data/IndexRepositoryBenchmark.kt b/app/src/test/kotlin/com/looker/droidify/data/IndexRepositoryBenchmark.kt new file mode 100644 index 000000000..a8d5c0fe8 --- /dev/null +++ b/app/src/test/kotlin/com/looker/droidify/data/IndexRepositoryBenchmark.kt @@ -0,0 +1,120 @@ +package com.looker.droidify.data + +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import com.looker.droidify.assets +import com.looker.droidify.data.local.droidifyDb +import com.looker.droidify.data.local.sql.DroidifyDb +import com.looker.droidify.data.model.Fingerprint +import com.looker.droidify.sync.JsonParser +import com.looker.droidify.sync.v2.model.IndexV2 +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlin.math.pow +import kotlin.math.sqrt +import kotlin.test.Test +import kotlin.time.Duration +import kotlin.time.measureTime +import kotlin.time.measureTimedValue + +class IndexRepositoryBenchmark { + + private val fingerprint = Fingerprint("ab".repeat(32)) + + @Test + fun `benchmark fdroid index import`() = benchmark("fdroid_index_v2.json") + + @Test + fun `benchmark izzy index import`() = benchmark("izzy_index_v2.json") + + private fun benchmark(asset: String) { + val (index, parseTime) = measureTimedValue { + JsonParser.decodeFromString( + assets(asset)!!.readBytes().decodeToString(), + ) + } + + repeat(WARMUP) { insertIndex(index) } + val timings = List(ITERATIONS) { insertIndex(index) } + report(asset, index.packages.size, parseTime, timings) + } + + private fun insertIndex(index: IndexV2): Duration { + val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + driver.use { driver -> + DroidifyDb.Schema.create(driver) + val db = droidifyDb(driver) + val repository = IndexRepository(db, Dispatchers.Unconfined) + val repoId = insertBareRepo(db) + return measureTime { + runBlocking { repository.insertIndex(repoId, fingerprint, index) } + } + } + } + + private fun insertBareRepo(db: DroidifyDb): Long { + db.repositoryQueries.insertRepo( + address = "https://repo.test/repo", + webBaseUrl = null, + fingerprint = null, + etag = null, + timestamp = null, + enabled = true, + ) + return db.repositoryQueries.lastInsertRowId().executeAsOne() + } + + private fun report(name: String, packages: Int, parse: Duration, timings: List) { + val ms = timings.map { it.inWholeMicroseconds / 1000.0 }.sorted() + val n = ms.size + val mean = ms.average() + val stddev = sqrt(ms.sumOf { (it - mean).pow(2) } / (n - 1)) + val ci95 = t95(n - 1) * stddev / sqrt(n.toDouble()) + val median = percentile(ms, 50.0) + + println( + """ + [$name] $packages packages, parse ${parse.fmt()} + runs $n (+$WARMUP warmup) + mean ${mean.fmt()} ± ${ci95.fmt()} (95% CI) + stddev ${stddev.fmt()} (cv ${"%.1f".format(stddev / mean * 100)}%) + median ${median.fmt()} p90 ${percentile(ms, 90.0).fmt()} + range ${ms.first().fmt()} … ${ms.last().fmt()} + rate ${"%.0f".format(packages / (median / 1000))} packages/s (median) + """.trimIndent(), + ) + } + + /** Linear-interpolated percentile over a sorted sample. */ + private fun percentile(sorted: List, p: Double): Double { + val rank = p / 100 * (sorted.size - 1) + val low = rank.toInt() + val high = minOf(low + 1, sorted.size - 1) + return sorted[low] + (sorted[high] - sorted[low]) * (rank - low) + } + + /** Two-tailed Student's t critical value at 95% confidence. */ + private fun t95(df: Int): Double = when { + df <= 1 -> 12.706 + df == 2 -> 4.303 + df == 3 -> 3.182 + df == 4 -> 2.776 + df == 5 -> 2.571 + df == 6 -> 2.447 + df == 7 -> 2.365 + df == 8 -> 2.306 + df == 9 -> 2.262 + df <= 15 -> 2.131 + df <= 30 -> 2.042 + else -> 1.960 + } + + private fun Double.fmt(): String = + if (this >= 1000) "%.3f s".format(this / 1000) else "%.1f ms".format(this) + + private fun Duration.fmt(): String = (inWholeMicroseconds / 1000.0).fmt() + + private companion object { + const val WARMUP = 2 + const val ITERATIONS = 10 + } +} diff --git a/app/src/test/kotlin/com/looker/droidify/data/IndexRepositoryTest.kt b/app/src/test/kotlin/com/looker/droidify/data/IndexRepositoryTest.kt new file mode 100644 index 000000000..2651b6424 --- /dev/null +++ b/app/src/test/kotlin/com/looker/droidify/data/IndexRepositoryTest.kt @@ -0,0 +1,236 @@ +@file:OptIn(ExperimentalStdlibApi::class) + +package com.looker.droidify.data + +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import com.looker.droidify.data.local.droidifyDb +import com.looker.droidify.data.local.sql.DroidifyDb +import com.looker.droidify.data.model.Fingerprint +import com.looker.droidify.sync.v2.model.AntiFeatureV2 +import com.looker.droidify.sync.v2.model.ApkFileV2 +import com.looker.droidify.sync.v2.model.CategoryV2 +import com.looker.droidify.sync.v2.model.FeatureV2 +import com.looker.droidify.sync.v2.model.FileV2 +import com.looker.droidify.sync.v2.model.IndexV2 +import com.looker.droidify.sync.v2.model.ManifestV2 +import com.looker.droidify.sync.v2.model.MetadataV2 +import com.looker.droidify.sync.v2.model.MirrorV2 +import com.looker.droidify.sync.v2.model.PackageV2 +import com.looker.droidify.sync.v2.model.PermissionV2 +import com.looker.droidify.sync.v2.model.RepoV2 +import com.looker.droidify.sync.v2.model.ScreenshotsV2 +import com.looker.droidify.sync.v2.model.UsesSdkV2 +import com.looker.droidify.sync.v2.model.VersionV2 +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class IndexRepositoryTest { + + private lateinit var driver: SqlDriver + private lateinit var db: DroidifyDb + private lateinit var indexRepository: IndexRepository + + private val fingerprint = Fingerprint("ab".repeat(32)) + + @BeforeTest + fun setup() { + driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + DroidifyDb.Schema.create(driver) + db = droidifyDb(driver) + indexRepository = IndexRepository(db, Dispatchers.Unconfined) + } + + @AfterTest + fun teardown() { + driver.close() + } + + @Test + fun `insertIndex populates all tables`() = runTest { + val repoId = insertBareRepo() + + indexRepository.insertIndex(repoId, fingerprint, testIndex(), etag = "etag-1") + + assertEquals(1, count("app")) + assertEquals(1, count("localized_app")) + assertEquals(1, count("localized_repo")) + assertEquals(1, count("author")) + assertEquals(1, count("links")) + assertEquals(2, count("mirror")) + assertEquals(1, count("category")) + assertEquals(1, count("category_repo_relation")) + assertEquals(1, count("category_app_relation")) + assertEquals(1, count("anti_feature")) + assertEquals(1, count("anti_feature_repo_relation")) + assertEquals(1, count("anti_features_app_relation")) + assertEquals(1, count("version")) + assertEquals(2, count("permission")) + assertEquals(1, count("feature")) + assertEquals(1, count("native_code")) + assertEquals(2, count("screenshot")) + // 1 feature graphic + 1 video + assertEquals(2, count("graphic")) + // 1 regular + bitcoin + liberapay + assertEquals(3, count("donate")) + + val appId = db.appQueries.selectAppId("com.example.app", repoId).executeAsOne() + val versionId = db.versionQueries + .selectVersionId(appId, "12".repeat(32).hexToByteArray()) + .executeAsOne() + assertNotNull(versionId) + } + + @Test + fun `insertIndex updates repository version info`() = runTest { + val repoId = insertBareRepo() + + indexRepository.insertIndex(repoId, fingerprint, testIndex(), etag = "etag-1") + + assertEquals( + 1, + count( + "repository", + where = "timestamp = 1720000000000 AND etag = 'etag-1' " + + "AND fingerprint IS NOT NULL", + ), + ) + } + + @Test + fun `insertIndex twice is idempotent`() = runTest { + val repoId = insertBareRepo() + + indexRepository.insertIndex(repoId, fingerprint, testIndex()) + indexRepository.insertIndex(repoId, fingerprint, testIndex()) + + assertEquals(1, count("app")) + assertEquals(1, count("author")) + assertEquals(1, count("version")) + assertEquals(2, count("permission")) + assertEquals(2, count("mirror")) + assertEquals(2, count("screenshot")) + assertEquals(3, count("donate")) + } + + @Test + fun `version without usesSdk falls back to defaults`() = runTest { + val repoId = insertBareRepo() + val index = testIndex { manifest -> + manifest.copy(usesSdk = null) + } + + indexRepository.insertIndex(repoId, fingerprint, index) + + assertEquals(1, count("version", where = "minSdkVersion = 1 AND targetSdkVersion = 1")) + } + + private fun insertBareRepo(): Long { + db.repositoryQueries.insertRepo( + address = "https://repo.test/repo", + webBaseUrl = null, + fingerprint = null, + etag = null, + timestamp = null, + enabled = true, + ) + return db.repositoryQueries.lastInsertRowId().executeAsOne() + } + + private fun count(table: String, where: String? = null): Long { + val sql = buildString { + append("SELECT COUNT(*) FROM ") + append(table) + if (where != null) { + append(" WHERE ") + append(where) + } + } + return driver.executeQuery( + identifier = null, + sql = sql, + mapper = { cursor -> + cursor.next() + QueryResult.Value(requireNotNull(cursor.getLong(0))) + }, + parameters = 0, + ).value + } + + private fun testIndex( + manifestTransform: (ManifestV2) -> ManifestV2 = { it }, + ): IndexV2 = IndexV2( + repo = RepoV2( + address = "https://repo.test/repo", + name = mapOf("en-US" to "Test Repo"), + description = mapOf("en-US" to "Test repository"), + icon = mapOf("en-US" to FileV2("/icons/repo.png", "cd".repeat(32), 128)), + mirrors = listOf( + MirrorV2("https://repo.test/repo", isPrimary = true), + MirrorV2("https://mirror.test/repo", countryCode = "US"), + ), + categories = mapOf( + "Internet" to CategoryV2(name = mapOf("en-US" to "Internet")), + ), + antiFeatures = mapOf( + "Ads" to AntiFeatureV2(name = mapOf("en-US" to "Advertising")), + ), + timestamp = 1720000000000, + ), + packages = mapOf( + "com.example.app" to PackageV2( + metadata = MetadataV2( + name = mapOf("en-US" to "Example"), + summary = mapOf("en-US" to "An example app"), + description = mapOf("en-US" to "A longer description"), + icon = mapOf("en-US" to FileV2("/icon.png", "ef".repeat(32), 64)), + added = 1710000000000, + lastUpdated = 1719000000000, + authorName = "Author", + authorEmail = "author@example.com", + categories = listOf("Internet"), + donate = listOf("https://donate.example.com"), + bitcoin = "bc1qexample", + liberapay = "example", + license = "GPL-3.0-only", + sourceCode = "https://git.example.com", + preferredSigner = "ab".repeat(32), + featureGraphic = mapOf("en-US" to FileV2("/fg.png")), + video = mapOf("en-US" to "https://video.example.com"), + screenshots = ScreenshotsV2( + phone = mapOf("en-US" to listOf(FileV2("/s1.png"), FileV2("/s2.png"))), + ), + ), + versions = mapOf( + "12".repeat(32) to VersionV2( + added = 1719000000000, + file = ApkFileV2("/app.apk", "12".repeat(32), 1024), + whatsNew = mapOf("en-US" to "Release notes"), + manifest = manifestTransform( + ManifestV2( + versionName = "1.0", + versionCode = 100, + usesSdk = UsesSdkV2(23, 34), + usesPermission = listOf( + PermissionV2("android.permission.INTERNET"), + ), + usesPermissionSdk23 = listOf( + PermissionV2("android.permission.BLUETOOTH", 30), + ), + features = listOf(FeatureV2("android.hardware.camera")), + nativecode = listOf("arm64-v8a"), + ), + ), + antiFeatures = mapOf("Tracking" to mapOf("en-US" to "Has tracking")), + ), + ), + ), + ), + ) +} From be2e820e69c079e63c0e1be7b5aad39985be4aab Mon Sep 17 00:00:00 2001 From: LooKeR Date: Mon, 20 Jul 2026 23:06:15 +0530 Subject: [PATCH 16/19] build: Enable parallel sync on Gradle 9.4+ and upgrade AGP --- gradle.properties | 1 + gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6104066c9..cc50cdce8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,3 +10,4 @@ org.gradle.configuration-cache=true org.gradle.daemon=true org.gradle.jvmargs=-Xmx6g -Xms256m -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC android.disallowKotlinSourceSets=false +org.gradle.tooling.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6fced65c7..3e94c6311 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.2.1" +agp = "9.3.0" material = "1.13.0" activity = "1.12.4" app-compat = "1.7.1" From 97a07c81ce8c47922d2778aea29365fd5fc6e86f Mon Sep 17 00:00:00 2001 From: LooKeR Date: Mon, 20 Jul 2026 23:09:11 +0530 Subject: [PATCH 17/19] refactor: Rearrang some code and create 1.db schema --- .../droidify/data/local/DroidifyDbFactory.kt | 38 +++++++++--------- app/src/main/sqldelight/databases/1.db | Bin 143360 -> 143360 bytes 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/src/main/kotlin/com/looker/droidify/data/local/DroidifyDbFactory.kt b/app/src/main/kotlin/com/looker/droidify/data/local/DroidifyDbFactory.kt index 81b19edce..75dc64f36 100644 --- a/app/src/main/kotlin/com/looker/droidify/data/local/DroidifyDbFactory.kt +++ b/app/src/main/kotlin/com/looker/droidify/data/local/DroidifyDbFactory.kt @@ -17,6 +17,25 @@ import com.looker.droidify.data.model.Fingerprint import com.looker.droidify.sync.JsonParser import com.looker.droidify.sync.v2.model.LocalizedString +fun droidifyDb(driver: SqlDriver): DroidifyDb = DroidifyDb( + driver = driver, + anti_features_app_relationAdapter = Anti_features_app_relation.Adapter( + reasonAdapter = localizedStringAdapter, + ), + authenticationAdapter = Authentication.Adapter(passwordAdapter = encryptedAdapter), + donateAdapter = Donate.Adapter(typeAdapter = IntColumnAdapter), + graphicAdapter = Graphic.Adapter(typeAdapter = IntColumnAdapter), + permissionAdapter = Permission.Adapter(maxSdkVersionAdapter = IntColumnAdapter), + repositoryAdapter = Repository.Adapter(fingerprintAdapter = fingerprintAdapter), + screenshotAdapter = Screenshot.Adapter(typeAdapter = IntColumnAdapter), + versionAdapter = Version.Adapter( + whatsNewAdapter = localizedStringAdapter, + maxSdkVersionAdapter = IntColumnAdapter, + minSdkVersionAdapter = IntColumnAdapter, + targetSdkVersionAdapter = IntColumnAdapter, + ), +) + private val localizedStringAdapter = object : ColumnAdapter { override fun decode(databaseValue: String): LocalizedString = JsonParser.decodeFromString(databaseValue) @@ -37,22 +56,3 @@ private val encryptedAdapter = object : ColumnAdapter { override fun encode(value: Encrypted): String = value.value } - -fun droidifyDb(driver: SqlDriver): DroidifyDb = DroidifyDb( - driver = driver, - anti_features_app_relationAdapter = Anti_features_app_relation.Adapter( - reasonAdapter = localizedStringAdapter, - ), - authenticationAdapter = Authentication.Adapter(passwordAdapter = encryptedAdapter), - donateAdapter = Donate.Adapter(typeAdapter = IntColumnAdapter), - graphicAdapter = Graphic.Adapter(typeAdapter = IntColumnAdapter), - permissionAdapter = Permission.Adapter(maxSdkVersionAdapter = IntColumnAdapter), - repositoryAdapter = Repository.Adapter(fingerprintAdapter = fingerprintAdapter), - screenshotAdapter = Screenshot.Adapter(typeAdapter = IntColumnAdapter), - versionAdapter = Version.Adapter( - whatsNewAdapter = localizedStringAdapter, - maxSdkVersionAdapter = IntColumnAdapter, - minSdkVersionAdapter = IntColumnAdapter, - targetSdkVersionAdapter = IntColumnAdapter, - ), -) diff --git a/app/src/main/sqldelight/databases/1.db b/app/src/main/sqldelight/databases/1.db index 89406969e2664d255ce3c728235f83b645a94038..1e39da4147eeb78f90b4089aad8d6e4f22fee302 100644 GIT binary patch delta 1196 zcmeHGO=}ZT6rD4Z$t07Rd^8Qyd^MA3zwo2ef~5+@Drg0%*mhx6N)yvqLu-uc->Ou+%x^U57piZWBsi1C!`|jc0cX$`hy=P+G zGO=#Ck@eZ;#Z#7UQ4wd*>1U2TAeI_vQO5?!uG3*^m`)PUR7HbDsnk#`wC z#$&m}U^;v8CrV}FGl+$^?XVFS>m?d)!Af4C9io1J0}J`gZr3a2w;Jvymp^DG9!o6* z>ZpNI>ak1%0$H=RhAa%jln*o2!wZVrjcM8F8Ovn!Fqbs(0N73wdCo<^lP}!804FGZ zKc@N&+Sx=bHRM3Kdm!RfPt>&N{ZZ5y5K?e!5bNZ~hY%g5vceZ#A}MAf3|pelvT>RogOLq*GE+ zNmA0MfYVs4e49ZX%*Hb3lJPBYk(B(t>YqG0XJQ!aq)+Ry=MQb^qv_rF%JS88D1s)0 z4J{TU*vD=Q(!NF*MLtZNb~*P48Ov$D&LL^3sy7&lVC?!o?SB9o>= aVFc+~7#@fvsNX|H)mc=vqS~(eMvotR%}@yd delta 1114 zcmeHG?MqWp7{9-Jw|npIcDK1D*S)#B%ok8MEd)OVO~cF}F(MKnbm7FQZEKrp^u;Om z#V@Y&;eb%#BB&^$hWJU8=!0Jj!CoLpge(Z6mw%u-E`td4F9@D<9?tJP=l2|*=lM-+ z$rD@hY|3ppWlkz?*$)H-u;WHrBWe#X(B!?Lv0UMh1*w*vY);A}hw*u#!G8 z3>!PYf=b4uLk!uuTaa!F+0zzMFdt<2Yx6^|S<*ZnOkT)DE{;S8qLIvK>W{OKTb2h< zNQbJ!Oor4|uc;V({c4t7I`AcBr~ayR9e8FH`3WEltHBTtFQtC9jb;7Bb}+nHUyfift!Id zHF#UbOPZ!(s;x*e5{nO~i<^-9z6p?ZM#7U%xFfQR4pl86{L`P;`|@4(yxveWdyT3% zIF;Y#Y)7pp)UN%j>|s@ww3Zf3Uj6;;cqZ1nTQJszj{jX4iSF5jdqGuP;hbEWGtVeF z(_?u_S{7Fg@5L$MEB}oj#23t?lUw7`wjS$8Ya*9x5Mn*G(i4=Vif{)IH;MU!uR_{dgpRiOkndsk9M;IVRI!&_ef!Dv gIBl=TXTGAGE(LKC0V;;j1El~voBXcuUz6YP3t)6X9{>OV From 90eea26a7d002537c5cc6bfa4f5fbeb7c97cdc21 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Wed, 29 Jul 2026 07:58:02 +0530 Subject: [PATCH 18/19] feat: Add installed and app preferences table Signed-off-by: LooKeR --- .../droidify/data/local/sql/AppPreferences.sq | 17 +++++++++++++++++ app/src/main/sqldelight/databases/1.db | Bin 143360 -> 159744 bytes 2 files changed, 17 insertions(+) create mode 100644 app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppPreferences.sq diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppPreferences.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppPreferences.sq new file mode 100644 index 000000000..9aa64b18d --- /dev/null +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/AppPreferences.sq @@ -0,0 +1,17 @@ +import kotlin.Boolean; + +CREATE TABLE preferences ( + preferredRepoId INTEGER REFERENCES repository(id) ON DELETE SET NULL, + -- NULL -> NOTHING + -- -1 -> IGNORE ALL + -- Number -> IGNORE + ignoreUpdate INTEGER, + favorite INTEGER AS Boolean NOT NULL DEFAULT 0, + packageName TEXT NOT NULL PRIMARY KEY +); + +CREATE TABLE installed ( + versionCode INTEGER NOT NULL, + signature BLOB NOT NULL, + packageName TEXT NOT NULL PRIMARY KEY +); diff --git a/app/src/main/sqldelight/databases/1.db b/app/src/main/sqldelight/databases/1.db index 1e39da4147eeb78f90b4089aad8d6e4f22fee302..43f6583145b686c4a941b0666fad31aafb19dd35 100644 GIT binary patch delta 2022 zcmeHIUrbw781L`4y|>)9w5+fTZCOuST0m%_E*N5#2`m>@sRawfe^J)i%k0M5?ouX; zCakdeGR=n9O9y>4AECM%?yHLvn*(EY}y{S7DTtj zUGp8kEPQXi!JjbxYP4_zPz|qD_LiQi)1DVvuuogzo3xLGtH#nJPHoK^K%MrD^?k2H zkUSnNJY7s5pH`-nba7VQo}Ahnw)ar0#q9(R?WFxHuU%Ju>5Q&?R#9iN`P`pV?e*jV zT*HH~k(As&E|c;8frLzUVA7_2TH7kkWKZOD8^}On zWPl_`#z}G_kHDRb`@(Nf(uE zD=IflrO)QoY!M0_4#SP^Di`&Lyc7?XUF5BL+GMQi$GSKmXcLw&EcDwTq&3ZQ5wK`Y zVy{%QFvbg#%Y}<}-Gk{ufpuZ6AohcM%uKI@(FmTt8^$_J^kgdt%=A(#et?k{+L$RO zdZ`U}8aY9dBrMAMA=BC1sTqARF8#6rXkW8KIPE9A#WSAP*V)(5x8++U$5uz3tHfC~ zcCQlh>Zqx!jxpMCsCLjkx{LI`nQv5){^c_5caz>@<@c5E`KM%$!-4;cts^@Wt!C@q zhRZWz`(B0vy&OR3{{Z-J0q{=LE;%!n|-OjnNx#wJ%T90wBU!E)P_BKQKGMh*h}2>lMrrkFYKqJ1HEX{R!j{v z*MSdArF?@xeVyn*V;R&ojBlf!IwSBxV!)ShX;bDt?AHs;y7^63a*pwR3|--Gnpn~L zU7Hhv|F#arxY-sOk06XzT8JRYJ?^JJ5`@q~t$rK?p%3}djbPa!$>5_tUW&PJI-AQV zbNVpJQrKjAw)k9Ljc3?8Ko`3ZBL{dX;)1eEPt}t?)AG|>UFhWIn&~Nm4t9y|?hgs> vHZrHpT*1qXaYNQf=+lulJ#sEDBz8f}D93mA+nCcX`*_nv!m?#cJ%o_yb(UbRiH zT5tP3L5}0Lu=Q&M=R`Pjzn0B`S)7+4k|ox#8q+Dt&RR1a2el`~;_Q$W;XwojQaELhhFpG-Ui#fTJB9Y0k zX(?z@1iWsdZ%42hX5$VPA<)Ynmf2*)`G?>QyP!faLc*H>M8)sNwM(S{my_AC)M$Er z;|p%nBNUz7;K-|)yrwN_i0_bkIfrnIb82#jWBTvTDt~a6xvu|r{m-s1L6gllxe6}D z*=wZJwn6KNc;0fuydXR=ALpOpIa;_cT*_5&#huH}M)|qi$t-Q+C#wlD>_!cFbXIJg zF*Ne0d~_{}Z3xg}6njxct5In17ZoufH>Hs2D0VV&-BGLra+fxWiqD7HmQp>8q_Wx3 zY#Du&i{K+FsK1rzm8Wy9m;fqi?J$}hRSti-B6H(jYHEXujnvnMcm9(b%JFQactfM< z#CWodmBw}Z3wR9Op`yY_JH7_orBYl^eLkDWTu2SBZyqBpf2{h~E`4QUpFxQBbYTDQ z8nv3AP{|d=c5>^?m}61wZ2kl;?@7G? From 67aebe0c929d94ffc44d76aec91206bbea099107 Mon Sep 17 00:00:00 2001 From: LooKeR Date: Wed, 29 Jul 2026 08:15:45 +0530 Subject: [PATCH 19/19] feat: Copy rblog and download stats table from legacy Signed-off-by: LooKeR --- .../looker/droidify/data/local/sql/Privacy.sq | 52 ++++++++++++++++++ app/src/main/sqldelight/databases/1.db | Bin 159744 -> 180224 bytes 2 files changed, 52 insertions(+) create mode 100644 app/src/main/sqldelight/com/looker/droidify/data/local/sql/Privacy.sq diff --git a/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Privacy.sq b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Privacy.sq new file mode 100644 index 000000000..853067727 --- /dev/null +++ b/app/src/main/sqldelight/com/looker/droidify/data/local/sql/Privacy.sq @@ -0,0 +1,52 @@ +import kotlin.Boolean; + +CREATE TABLE rb_log ( + packageName TEXT NOT NULL, + versionCode INTEGER NOT NULL, + versionName TEXT NOT NULL, + apkUrl TEXT NOT NULL, + tag TEXT NOT NULL, + commitHash TEXT NOT NULL, + timestamp INTEGER NOT NULL, + -- NULL -> rebuild attempted, verdict unknown + reproducible INTEGER AS Boolean, + error TEXT, + hash TEXT NOT NULL, + repository TEXT NOT NULL, + PRIMARY KEY(hash, repository) +); + +CREATE INDEX index_rb_log_packageName ON rb_log(packageName, versionCode); + +CREATE TABLE download_stats ( + downloads INTEGER NOT NULL, + timestamp INTEGER NOT NULL, + packageName TEXT NOT NULL, + source TEXT NOT NULL, + PRIMARY KEY(packageName, source) +) WITHOUT ROWID; + +insertRBLog: +INSERT OR REPLACE INTO rb_log(packageName, versionCode, versionName, apkUrl, tag, commitHash, timestamp, reproducible, error, hash, repository) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + +rbLogs: +SELECT * +FROM rb_log +WHERE packageName = ? +ORDER BY versionCode DESC, timestamp DESC; + +deleteAllRBLogs: +DELETE FROM rb_log; + +insertDownloadStats: +INSERT OR REPLACE INTO download_stats(downloads, timestamp, packageName, source) +VALUES (?, ?, ?, ?); + +totalDownloads: +SELECT COALESCE(SUM(downloads), 0) +FROM download_stats +WHERE packageName = ?; + +deleteAllDownloadStats: +DELETE FROM download_stats; diff --git a/app/src/main/sqldelight/databases/1.db b/app/src/main/sqldelight/databases/1.db index 43f6583145b686c4a941b0666fad31aafb19dd35..e300b402054b042223533f5dd04537fd0e8acd94 100644 GIT binary patch delta 1415 zcmds1O>7%g5Z+n;ti4YBqb97KKfmJs*u+UUq^h7%gq>vT82P7)92!-X%*MNFH?h6$ zdJRPa5hnt1pon6L>Di|WMM5PKkvP&yz=a=GNF2+7E8>89Ac*jGovKMC;LekF_RY+j z_rCdN-rIQS-PrJbI2?*G43nha;}N@+oF0rZlQ;KV)WilJS&ruddx1xH&CLK$mBAq@ z2K<}B&x7D34!^^*%Zczu;0*T((+3m~~p`Ti|epCT~qTYtFD+ZmcgG)lSGYfSP!H zASG#~)YGpW9LCcSy{VbH@rI4Z;v-@?ljMnqk}*E$7G8?r{yc<*UWVnV-aqWU$9Sx^ z+uhH=F_v|FbL;lR+vZ75n2kU?s?`gx$WeW?^3i+qCq*x#a>ksW< z^U&1v`n;~FiIo#>VR{&@Yj`sUV{jU`b1)4te3%0fPGKkyMR*qFJZ;mHxS5A3a@;LU z90T2y9LI?wJRt4sqjrC6 z&D#A@(=5V!l34knD8iMyzlB-gcObZ5?24pIf?XnZNvbP~RQ-rJiJ#kM{IB|B{b%|_ z-yPp1|1GV=3)-cvc_r5r(lv^P8;FJDHx|2 zf0=?sNT4qbm;YBKnSiHWqOt12B9kj!G&~Jd=Wu+iMq59 zp)j{Mt&4061uHbrjSIW%qEG}^rGG)ijU|Q>zaSlE7UwsI!%V4kzx2Da5)Vot#HgD8 zB@f3RhozW&({w8Yz1d!b^`T-?>Yh3v~ksYoce8~74{OKL&!w(c=;qfdAPqDNSw zqv2_JjC(2fgey&pS_7|ibnW