Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bridge the communication between FCT and FHIRCore app #3556

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
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
6 changes: 6 additions & 0 deletions android/quest/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,11 @@
<meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer"
tools:node="remove" />
</provider>

<provider
android:authorities="${applicationId}.fct"
android:name=".fct.FCTContentProvider"
android:exported="true" />

</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package org.smartregister.fhircore.quest.fct

import com.google.android.fhir.FhirEngine
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import org.smartregister.fhircore.engine.data.remote.model.response.UserInfo
import org.smartregister.fhircore.engine.util.SecureSharedPreference
import org.smartregister.fhircore.engine.util.SharedPreferenceKey
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
import org.smartregister.fhircore.engine.util.extension.countUnSyncedResources
import org.smartregister.fhircore.engine.util.extension.encodeJson
import org.smartregister.fhircore.quest.BuildConfig

class AllInsights(
private val fhirEngine: FhirEngine,
private val dbBridge: DatabaseBridge,
private val sharedPreferencesHelper: SharedPreferencesHelper,
private val secureSharedPreference: SecureSharedPreference,
) {

fun execute(arg: String): String {

return try {

val resourceTypeCount = getResourceTypeCount()
val unSyncedResources = runBlocking { fhirEngine.countUnSyncedResources() }

Response(
error = null,
result = Insight(
resourceTypeCount = resourceTypeCount,
unSyncedResources = unSyncedResources,
userInfo = retrieveUserInfo(),
userName = retrieveUsername(),
organization = retrieveOrganization(),
careTeam = retrieveCareTeam(),
location = practitionerLocation(),
appVersionCode = BuildConfig.VERSION_CODE.toString(),
appVersion = BuildConfig.VERSION_NAME,
buildDate = BuildConfig.BUILD_DATE,
)
).encodeJson()

} catch (ex: Exception) {
return Response(
error = ex.message ?: "Query Error"
).encodeJson()
}

}

private fun getResourceTypeCount(): Map<String, Int> {
val query =
"SELECT resourceType, COUNT(*) as count FROM ResourceEntity GROUP BY resourceType"
val cursor = dbBridge.runQuery(query)

val result = mutableMapOf<String, Int>()
if (cursor.moveToFirst()) {
do {
val resourceTypeColIndex = cursor.getColumnIndex("resourceType")
val countColIndex = cursor.getColumnIndex("count")

val key = cursor.getString(resourceTypeColIndex)
val value = cursor.getInt(countColIndex)

result[key] = value
} while (cursor.moveToNext())
}

return result
}

private fun retrieveUserInfo() =
sharedPreferencesHelper.read<UserInfo>(
key = SharedPreferenceKey.USER_INFO.name,
)

private fun retrieveUsername(): String? = secureSharedPreference.retrieveSessionUsername()

private fun retrieveOrganization() =
sharedPreferencesHelper.read(SharedPreferenceKey.ORGANIZATION.name, null)

private fun retrieveCareTeam() =
sharedPreferencesHelper.read(SharedPreferenceKey.CARE_TEAM.name, null)

private fun practitionerLocation() =
sharedPreferencesHelper.read(SharedPreferenceKey.PRACTITIONER_LOCATION.name, null)

@Serializable
private data class Insight(
val resourceTypeCount: Map<String, Int>,
val unSyncedResources: List<Pair<String, Int>>,
val userInfo: UserInfo?,
val userName: String?,
val organization: String?,
val careTeam: String?,
val location: String?,
val appVersionCode: String,
val appVersion: String,
val buildDate: String,
)

@Serializable
private data class Response(
var error: String?,
val result: Insight? = null,
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package org.smartregister.fhircore.quest.fct

import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import androidx.core.database.getBlobOrNull
import androidx.core.database.getFloatOrNull
import androidx.core.database.getIntOrNull
import androidx.core.database.getStringOrNull
import com.google.android.fhir.FhirEngine
import kotlinx.serialization.Serializable
import okio.ByteString.Companion.toByteString
import org.json.JSONArray
import org.json.JSONObject
import org.smartregister.fhircore.engine.util.extension.decodeJson

class DatabaseBridge(
private val context: Context,
private val fhirEngine: FhirEngine,
) {

fun execute(arg: String): String {
val queryRequest = arg.decodeJson<QueryRequest>()

val dbHelper = DBHelper(context, queryRequest.database, getDatabaseVersion())
val db = dbHelper.writableDatabase

val query = queryRequest.query
val result = when (getQueryType(query)) {
QueryType.SELECT, QueryType.UNKNOWN -> {

var cursor: Cursor? = null
val jsonObject = JSONObject()
try {
cursor = db.rawQuery(query, null)
extractAllRecords(cursor, queryRequest, jsonObject)
} catch (ex: Exception) {
jsonObject.put("success", false)
jsonObject.put("error", ex.localizedMessage ?: ex.message ?: ex.toString())
} finally {
cursor?.close()
}
jsonObject.put("method", "rawQuery")
jsonObject.toString()
}

else -> {
val jsonObject = JSONObject()
try {
db.execSQL(query)
jsonObject.put("success", true)
} catch (ex: Exception) {
jsonObject.put("success", false)
jsonObject.put("error", ex.localizedMessage ?: ex.message ?: ex.toString())
}
jsonObject.put("method", "execSQL")
jsonObject.toString()
}
}

return result
}

fun runQuery(query: String) : Cursor {
val dbHelper = DBHelper(context, "resources.db", getDatabaseVersion())
val db = dbHelper.writableDatabase
return db.rawQuery(query, null)
}

private fun getQueryType(query: String): QueryType {
return when {
isFound(query, "^SELECT\\s+") -> QueryType.SELECT
isFound(query, "^INSERT\\s+") -> QueryType.INSERT
isFound(query, "^UPDATE\\s+") -> QueryType.UPDATE
isFound(query, "^DELETE\\s+") -> QueryType.DELETE
else -> QueryType.UNKNOWN
}
}

private fun isFound(query: String, pattern: String): Boolean {
return pattern.toRegex(RegexOption.IGNORE_CASE).find(query.trim()) != null
}

private fun extractAllRecords(
cursor: Cursor,
queryRequest: QueryRequest,
jsonObject: JSONObject
) {
val jsonArray = JSONArray()

jsonObject.put("count", cursor.count)

if (cursor.moveToPosition(queryRequest.offset)) {
do {
val obj = JSONObject()
cursor.columnNames.forEachIndexed { columnIndex, columName ->
when (cursor.getType(columnIndex)) {
Cursor.FIELD_TYPE_BLOB -> {
obj.put(
columName,
cursor.getBlobOrNull(columnIndex)?.toByteString()?.hex()
)
}

Cursor.FIELD_TYPE_INTEGER -> {
obj.put(columName, cursor.getIntOrNull(columnIndex))
}

Cursor.FIELD_TYPE_FLOAT -> {
obj.put(columName, cursor.getFloatOrNull(columnIndex))
}

else -> {
obj.put(columName, cursor.getStringOrNull(columnIndex))
}
}
}
jsonArray.put(obj)
} while (cursor.position < ((queryRequest.offset - 1) + queryRequest.limit) && cursor.moveToNext())
}

val columnsArray = JSONArray()
cursor.columnNames.forEach(columnsArray::put)

jsonObject.put("success", true)
jsonObject.put("data", jsonArray)
jsonObject.put("columnNames", columnsArray)
}


private fun getDatabaseVersion(): Int {
val fhirDatabase =
fhirEngine.javaClass.getDeclaredField("database").apply { isAccessible = true }
.get(fhirEngine)
val resourceDatabaseImpl =
fhirDatabase.javaClass.getDeclaredField("db").apply { isAccessible = true }
.get(fhirDatabase)
val sqliteOpenHelper =
resourceDatabaseImpl.javaClass.getMethod("getOpenHelper").invoke(resourceDatabaseImpl)
val supportSqliteDatabase =
sqliteOpenHelper.javaClass.getMethod("getReadableDatabase").invoke(sqliteOpenHelper)
val dbVersion = supportSqliteDatabase.javaClass.getDeclaredMethod("getVersion")
.invoke(supportSqliteDatabase)
return dbVersion as Int
}

@Serializable
private data class QueryRequest(
val database: String,
val query: String,
val sortColumn: String? = null,
val offset: Int,
val limit: Int,
)

private enum class QueryType {
SELECT, INSERT, UPDATE, DELETE, UNKNOWN
}

private class DBHelper(context: Context?, database: String, version: Int) :
SQLiteOpenHelper(context, database, null, version) {
override fun onCreate(db: SQLiteDatabase?) {}

override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {}

}
}
Loading
Loading