Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@
import org.piramalswasthya.sakhi.model.dynamicEntity.eye_surgery.EyeSurgeryFormResponseJsonEntity
import org.piramalswasthya.sakhi.model.dynamicEntity.filariaaMdaCampaign.FilariaMDACampaignFormResponseJsonEntity
import org.piramalswasthya.sakhi.model.dynamicEntity.mosquitonetEntity.MosquitoNetFormResponseJsonEntity

import org.piramalswasthya.sakhi.database.room.dao.GamificationDao
import org.piramalswasthya.sakhi.model.GamificationBadge
import org.piramalswasthya.sakhi.model.GamificationProfile
import org.piramalswasthya.sakhi.model.PointsTransaction
@Database(
entities = [
HouseholdCache::class,
Expand Down Expand Up @@ -200,10 +203,13 @@
FilariaMDAFormResponseJsonEntity::class,
ANCFormResponseJsonEntity::class,
FilariaMDACampaignFormResponseJsonEntity::class,
TBConfirmedTreatmentCache::class
TBConfirmedTreatmentCache::class,
GamificationProfile::class,
GamificationBadge::class,
PointsTransaction::class,
],
views = [BenBasicCache::class],
version = 57, exportSchema = false
version = 58, exportSchema = false
)

@TypeConverters(
Expand Down Expand Up @@ -268,6 +274,7 @@
abstract fun formResponseFilariaMDACampaignJsonDao(): FilariaMdaCampaignJsonDao

abstract val syncDao: SyncDao
abstract val gamificationDao: GamificationDao
Comment on lines 274 to +277

companion object {
@Volatile
Expand Down Expand Up @@ -311,6 +318,70 @@

})

val MIGRATION_57_58 = object : Migration(57, 58) {

Check warning on line 321 in app/src/main/java/org/piramalswasthya/sakhi/database/room/InAppDb.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this local variable to match the regular expression ^`?[_a-z][a-zA-Z0-9]*`?$

See more on https://sonarcloud.io/project/issues?id=PSMRI_FLW-Mobile-App&issues=AZ4S8eR_Q47wmykSSIu7&open=AZ4S8eR_Q47wmykSSIu7&pullRequest=439
override fun migrate(database: SupportSQLiteDatabase) {

if (!tableExists(database, "GAMIFICATION_PROFILE")) {
database.execSQL("""
CREATE TABLE `GAMIFICATION_PROFILE` (
`userId` INTEGER NOT NULL,
`totalPoints` INTEGER NOT NULL DEFAULT 0,
`currentStreakDays` INTEGER NOT NULL DEFAULT 0,
`longestStreakDays` INTEGER NOT NULL DEFAULT 0,
`lastActivityDate` TEXT,
`lastLoginDate` TEXT,
`level` INTEGER NOT NULL DEFAULT 1,
`createdAt` INTEGER NOT NULL,
`updatedAt` INTEGER NOT NULL,
`syncState` INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY(`userId`)
)
""".trimIndent())
}

if (!tableExists(database, "GAMIFICATION_BADGE")) {
database.execSQL("""
CREATE TABLE `GAMIFICATION_BADGE` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`userId` INTEGER NOT NULL,
`badgeType` TEXT NOT NULL,
`badgeNameEn` TEXT NOT NULL,
`badgeNameHi` TEXT NOT NULL,
`badgeNameAs` TEXT NOT NULL,
`earnedAt` INTEGER NOT NULL,
`syncState` INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(`userId`) REFERENCES `GAMIFICATION_PROFILE`(`userId`)
ON UPDATE CASCADE ON DELETE CASCADE
)
""".trimIndent())
database.execSQL(
"CREATE INDEX IF NOT EXISTS `idx_gamification_badge_userId` ON `GAMIFICATION_BADGE` (`userId`)"
)
}

if (!tableExists(database, "GAMIFICATION_POINTS_TX")) {
database.execSQL("""
CREATE TABLE `GAMIFICATION_POINTS_TX` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`userId` INTEGER NOT NULL,
`points` INTEGER NOT NULL,
`eventType` TEXT NOT NULL,
`eventRefId` TEXT,
`reason` TEXT NOT NULL,
`createdAt` INTEGER NOT NULL,
`syncState` INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(`userId`) REFERENCES `GAMIFICATION_PROFILE`(`userId`)
ON UPDATE CASCADE ON DELETE CASCADE
)
""".trimIndent())
database.execSQL(
"CREATE INDEX IF NOT EXISTS `idx_gamification_tx_userId` ON `GAMIFICATION_POINTS_TX` (`userId`)"
)
Comment on lines +358 to +379

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'tableName = "GAMIFICATION_BADGE"|tableName = "GAMIFICATION_POINTS_TX"|indices = \[Index|CREATE INDEX IF NOT EXISTS .*GAMIFICATION_(BADGE|POINTS_TX)' \
  app/src/main/java/org/piramalswasthya/sakhi/model/GamificationBadge.kt \
  app/src/main/java/org/piramalswasthya/sakhi/model/PointsTransaction.kt \
  app/src/main/java/org/piramalswasthya/sakhi/database/room/InAppDb.kt

Repository: PSMRI/FLW-Mobile-App

Length of output: 919


🏁 Script executed:

# Check Room version in build files
find . -name "build.gradle*" -o -name "build.gradle.kts" | head -5 | xargs grep -l "room" 2>/dev/null | head -1 | xargs cat | grep -i "room" | head -20

Repository: PSMRI/FLW-Mobile-App

Length of output: 402


🏁 Script executed:

# Look for schema files or Room JSON schemas that might show expected index names
find . -name "*.json" -path "*/schemas/*" | head -10

Repository: PSMRI/FLW-Mobile-App

Length of output: 46


🏁 Script executed:

# Check full entity definitions to understand index usage pattern
cat -n app/src/main/java/org/piramalswasthya/sakhi/model/GamificationBadge.kt

Repository: PSMRI/FLW-Mobile-App

Length of output: 2082


🏁 Script executed:

# Check other index definitions in the codebase for naming patterns
rg -n 'indices = \[Index' app/src/main/java/org/piramalswasthya/sakhi/model/ | head -20

Repository: PSMRI/FLW-Mobile-App

Length of output: 3197


🏁 Script executed:

# Look for Room schema files (typically generated in build/schemas/)
find . -name "*.json" -type f | grep -i "schema" | head -10

Repository: PSMRI/FLW-Mobile-App

Length of output: 46


🏁 Script executed:

# Check if schema validation is configured or if there are tests
rg -l "schema.*validation|room.*test" --type kt --type java | head -10

Repository: PSMRI/FLW-Mobile-App

Length of output: 91


🏁 Script executed:

# Look at the migration class more broadly to understand context
sed -n '1,400p' app/src/main/java/org/piramalswasthya/sakhi/database/room/InAppDb.kt | tail -100

Repository: PSMRI/FLW-Mobile-App

Length of output: 5164


🏁 Script executed:

# Search for Room index documentation or validation behavior references
rg -n "Index.*name|@Index" app/src/main/java/org/piramalswasthya/sakhi/database/room/ | head -20

Repository: PSMRI/FLW-Mobile-App

Length of output: 229


🏁 Script executed:

# Search for Room schema export or validation configuration
rg -n "exportSchema|room.schemaLocation" --type java --type gradle | head -20

Repository: PSMRI/FLW-Mobile-App

Length of output: 46


🏁 Script executed:

# Check gradle build configuration for Room settings
find . -name "build.gradle*" | head -3 | xargs cat | grep -A 10 -B 2 "room"

Repository: PSMRI/FLW-Mobile-App

Length of output: 1244


🏁 Script executed:

# Look for any existing schema files
find . -type d -name "schemas" 2>/dev/null

Repository: PSMRI/FLW-Mobile-App

Length of output: 46


🏁 Script executed:

# Check Room documentation pattern - search for how indices are defined in similar migrations
rg -n "CREATE INDEX.*userId" app/src/main/java/org/piramalswasthya/sakhi/database/room/InAppDb.kt | head -20

Repository: PSMRI/FLW-Mobile-App

Length of output: 321


🏁 Script executed:

# Search for Room validation error messages or index-related issues in code
rg -n "index.*schema|schema.*validation|index.*mismatch" -i --type java | head -20

Repository: PSMRI/FLW-Mobile-App

Length of output: 46


🏁 Script executed:

# Check if there are any tests for migrations that might reveal validation behavior
find . -name "*Migration*Test*" -o -name "*InAppDb*Test*" 2>/dev/null | head -5

Repository: PSMRI/FLW-Mobile-App

Length of output: 46


🏁 Script executed:

# Look for any Room documentation or comments about index naming
rg -n "Index.*auto.*generate|Index.*name.*convention" -i | head -10

Repository: PSMRI/FLW-Mobile-App

Length of output: 46


🏁 Script executed:

# Verify what Room actually generates - check if there are any gradle outputs or compiled schema expectations
rg -n "index_GAMIFICATION_BADGE_userId|index_GAMIFICATION_POINTS_TX_userId|idx_gamification" | head -20

Repository: PSMRI/FLW-Mobile-App

Length of output: 459


🌐 Web query:

Room Android Index naming convention unnamed indices schema validation 2.6.1

💡 Result:

In Room, @Index name handling and schema validation behave as follows (Room 2.x, including 2.6.1): 1) “Unnamed index” naming convention - In @androidx.room.Index, the name property defaults to empty string. If name is not set, Room generates an index name based on the table name and indexed column list: it “will set it to the list of columns joined by ‘’ and prefixed by ‘index${tableName}’”. Example given: table Foo with columns {bar, baz} becomes index_Foo_bar_baz. [1] - Recommendation: if you need to reference the index in a query, don’t rely on the generated name; explicitly set name. [1] 2) How this relates to “unnamed indices schema validation” - Room’s schema JSON (used for schema validation during migrations) includes index information, but Room’s automatic migration schema validator is primarily about validating the table structure. An issue discussion notes that Room does not validate “whether indices are being created correctly” in that context. [2] - Practically, even if your index “name” was auto-generated (because you left @Index.name unset), the existence/definition of the index in the generated schema is still derived from the @Index/@entity indices metadata. The critical point for schema validation is that Room’s migration validator may not fully verify index creation details across migrations in some test/migration scenarios. [2] 3) Where Room 2.6.1 fits - Room 2.6.1 release notes mainly cover Gradle plugin/schema output behavior (schemaDirectory requirement, flavored schema output paths, committing schemas for validation/auto-migrations) and a validation-related fix around defaultValue parentheses. [3] - The index naming convention itself is defined by the @Index documentation and applies to the annotation behavior. [1] If you share your exact failing validation error (or whether you’re using the Room Gradle Plugin + schemaDirectory, and whether the missing item is the index name vs the index’s existence), I can pinpoint whether the issue is likely “auto-generated index name mismatch” or “index creation not validated by the migration validator”.

Citations:


Add explicit index names to match migration instead of relying on unnamed indices.

The entities declare unnamed indices (indices = [Index(value = ["userId"])]), which Room would auto-name as index_GAMIFICATION_BADGE_userId and index_GAMIFICATION_POINTS_TX_userId. However, the migration creates custom-named indices (idx_gamification_badge_userId and idx_gamification_tx_userId). This mismatch between the entity schema and migration could cause inconsistencies if auto-migrations or schema validation tools are introduced later.

The codebase pattern (e.g., DeliveryOutcome, TBSuspected, ReferalCache) explicitly names all indices. Align these two entities with that convention by adding explicit name parameters:

🛠️ Suggested fix
- indices = [Index(value = ["userId"])]
+ indices = [Index(name = "idx_gamification_badge_userId", value = ["userId"])]

For PointsTransaction.kt:

- indices = [Index(value = ["userId"])]
+ indices = [Index(name = "idx_gamification_tx_userId", value = ["userId"])]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/org/piramalswasthya/sakhi/database/room/InAppDb.kt` around
lines 357 - 378, Entities for the GAMIFICATION_BADGE and GAMIFICATION_POINTS_TX
tables declare unnamed indices which will be auto-named by Room and thus
mismatch the migration-created indices; update the entity classes (e.g.,
GamificationBadge and the PointsTransaction/GamificationPointsTx class) to
declare explicit Index annotations on userId with the exact names used in the
migration: for the badge entity use Index(value = ["userId"], name =
"idx_gamification_badge_userId") and for the points transaction entity use
Index(value = ["userId"], name = "idx_gamification_tx_userId"); this aligns the
entity schema with the migration and avoids future validation/automerge issues.

}
}
}


val MIGRATION_56_57 = object : Migration(56, 57) {
override fun migrate(database: SupportSQLiteDatabase) {
val householdLocColumns = listOf(
Expand Down Expand Up @@ -3214,7 +3285,8 @@
MIGRATION_53_54,
MIGRATION_54_55,
MIGRATION_55_56,
MIGRATION_56_57
MIGRATION_56_57,
MIGRATION_57_58


).build()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.piramalswasthya.sakhi.database.room.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
import org.piramalswasthya.sakhi.model.GamificationBadge
import org.piramalswasthya.sakhi.model.GamificationProfile
import org.piramalswasthya.sakhi.model.PointsTransaction

@Dao
interface GamificationDao {

// ── Profile ───────────────────────────────────────────────────────────────

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertProfile(profile: GamificationProfile)

@Query("SELECT * FROM GAMIFICATION_PROFILE WHERE userId = :userId")
suspend fun getProfile(userId: Int): GamificationProfile?

@Query("SELECT * FROM GAMIFICATION_PROFILE WHERE userId = :userId")
fun observeProfile(userId: Int): Flow<GamificationProfile?>

@Query("SELECT * FROM GAMIFICATION_PROFILE WHERE syncState = 0")
suspend fun getUnsyncedProfiles(): List<GamificationProfile>

@Query("UPDATE GAMIFICATION_PROFILE SET syncState = 2 WHERE userId = :userId")
suspend fun markProfileSynced(userId: Int)

// ── Badges ────────────────────────────────────────────────────────────────

@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertBadge(badge: GamificationBadge): Long

@Query("SELECT COUNT(*) FROM GAMIFICATION_BADGE WHERE userId = :userId AND badgeType = :badgeType")
suspend fun hasBadge(userId: Int, badgeType: String): Int

@Query("SELECT * FROM GAMIFICATION_BADGE WHERE userId = :userId ORDER BY earnedAt DESC")
fun observeBadges(userId: Int): Flow<List<GamificationBadge>>

@Query("SELECT * FROM GAMIFICATION_BADGE WHERE syncState = 0")
suspend fun getUnsyncedBadges(): List<GamificationBadge>

@Query("UPDATE GAMIFICATION_BADGE SET syncState = 2 WHERE id IN (:ids)")
suspend fun markBadgesSynced(ids: List<Long>)

// ── Points Transactions ───────────────────────────────────────────────────

@Insert
suspend fun insertTransaction(tx: PointsTransaction): Long

@Query("SELECT * FROM GAMIFICATION_POINTS_TX WHERE userId = :userId ORDER BY createdAt DESC LIMIT 20")
fun observeRecentTransactions(userId: Int): Flow<List<PointsTransaction>>

@Query("SELECT * FROM GAMIFICATION_POINTS_TX WHERE syncState = 0")
suspend fun getUnsyncedTransactions(): List<PointsTransaction>

@Query("UPDATE GAMIFICATION_POINTS_TX SET syncState = 2 WHERE id IN (:ids)")
suspend fun markTransactionsSynced(ids: List<Long>)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.piramalswasthya.sakhi.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.piramalswasthya.sakhi.database.room.InAppDb
import org.piramalswasthya.sakhi.database.room.dao.GamificationDao
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object GamificationModule {

@Provides
@Singleton
fun provideGamificationDao(db: InAppDb): GamificationDao =
db.gamificationDao
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.piramalswasthya.sakhi.gamification

/**
* Centralised catalogue of every badge the engine can award.
*
* Adding a new badge = one entry here + one rule in GamificationEngine.
* Nothing else changes.
*
* All three language names are stored here so the UI can render
* badges completely offline without any string resource lookup at runtime.
*/
object BadgeCatalog {

data class BadgeDef(
val type: String,
val nameEn: String,
val nameHi: String,
val nameAs: String,
val descEn: String
)

// ── First-time badges ─────────────────────────────────────────────────────

val FIRST_HOUSEHOLD = BadgeDef(
type = "FIRST_HOUSEHOLD",
nameEn = "First Home",
nameHi = "पहला घर",
nameAs = "প্ৰথম ঘৰ",
descEn = "Registered your first household"
)

val FIRST_BENEFICIARY = BadgeDef(
type = "FIRST_BENEFICIARY",
nameEn = "First Step",
nameHi = "पहला कदम",
nameAs = "প্ৰথম পদক্ষেপ",
descEn = "Registered your first beneficiary"
)

// ── Streak badges ─────────────────────────────────────────────────────────

val STREAK_3 = BadgeDef(
type = "STREAK_3",
nameEn = "3-Day Streak",
nameHi = "3 दिन की लय",
nameAs = "3 দিনৰ ধাৰা",
descEn = "Active for 3 days in a row"
)

val STREAK_7 = BadgeDef(
type = "STREAK_7",
nameEn = "Week Warrior",
nameHi = "सप्ताह योद्धा",
nameAs = "সপ্তাহৰ যোদ্ধা",
descEn = "Active for 7 days in a row"
)

val STREAK_30 = BadgeDef(
type = "STREAK_30",
nameEn = "Monthly Champion",
nameHi = "मासिक चैंपियन",
nameAs = "মাহিলী চেম্পিয়ন",
descEn = "Active for 30 days in a row"
)

// ── Health activity badges ────────────────────────────────────────────────

val ANC_HERO = BadgeDef(
type = "ANC_HERO",
nameEn = "ANC Hero",
nameHi = "ANC नायक",
nameAs = "ANC বীৰ",
descEn = "Completed your first ANC visit"
)

val IMMUNIZATION_GUARDIAN = BadgeDef(
type = "IMMUNIZATION_GUARDIAN",
nameEn = "Immunization Guardian",
nameHi = "टीकाकरण संरक्षक",
nameAs = "টিকাকৰণৰ ৰক্ষক",
descEn = "Recorded your first immunization"
)

val HRP_IDENTIFIER = BadgeDef(
type = "HRP_IDENTIFIER",
nameEn = "Life Saver",
nameHi = "जीवन रक्षक",
nameAs = "জীৱন ৰক্ষক",
descEn = "Identified your first high-risk pregnancy"
)

val NCD_CHAMPION = BadgeDef(
type = "NCD_CHAMPION",
nameEn = "NCD Champion",
nameHi = "NCD चैंपियन",
nameAs = "NCD চেম্পিয়ন",
descEn = "Completed your first NCD screening"
)

// ── Level badges ──────────────────────────────────────────────────────────

val LEVEL_2 = BadgeDef(
type = "LEVEL_2",
nameEn = "Rising Star",
nameHi = "उभरता सितारा",
nameAs = "উদীয়মান তৰা",
descEn = "Reached Level 2"
)

val LEVEL_5 = BadgeDef(
type = "LEVEL_5",
nameEn = "Health Champion",
nameHi = "स्वास्थ्य चैंपियन",
nameAs = "স্বাস্থ্য চেম্পিয়ন",
descEn = "Reached Level 5"
)
}
Loading