Skip to content
Merged
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 app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ dependencies {
implementation(libs.androidx.compose.foundation)
ksp(libs.room.compiler)

// DataStore
implementation(libs.androidx.datastore.preferences)

// WorkManager
implementation(libs.androidx.work.runtime.ktx)

// Retrofit & OkHttp (Networking)
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
Expand Down
25 changes: 25 additions & 0 deletions app/src/main/java/com/jksalcedo/librefind/LibreFindApp.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package com.jksalcedo.librefind

import android.app.Application
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.jksalcedo.librefind.di.appModule
import com.jksalcedo.librefind.di.networkModule
import com.jksalcedo.librefind.di.repositoryModule
import com.jksalcedo.librefind.di.supabaseModule
import com.jksalcedo.librefind.di.useCaseModule
import com.jksalcedo.librefind.di.viewModelModule
import com.jksalcedo.librefind.worker.SignerFeedWorker
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import java.util.concurrent.TimeUnit

class LibreFindApp : Application() {
override fun onCreate() {
Expand All @@ -20,5 +27,23 @@ class LibreFindApp : Application() {
androidContext(this@LibreFindApp)
modules(appModule, networkModule, repositoryModule, useCaseModule, viewModelModule, supabaseModule)
}

scheduleSignerFeedUpdate()
}

private fun scheduleSignerFeedUpdate() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

val workRequest = PeriodicWorkRequestBuilder<SignerFeedWorker>(1, TimeUnit.DAYS)
.setConstraints(constraints)
.build()

WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"signer_feed_update",
ExistingPeriodicWorkPolicy.KEEP,
workRequest
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.jksalcedo.librefind.data.local

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.google.gson.Gson
import com.jksalcedo.librefind.data.remote.model.RemoteSignerFeed
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "signer_feed")

class SignerFeedDataStore(private val context: Context, private val gson: Gson) {

companion object {
private val FEED_JSON_KEY = stringPreferencesKey("feed_json")
private val ETAG_KEY = stringPreferencesKey("etag")

}

val feedFlow: Flow<RemoteSignerFeed?> = context.dataStore.data.map { preferences ->
val json = preferences[FEED_JSON_KEY]
if (json != null) {
try {
gson.fromJson(json, RemoteSignerFeed::class.java)
} catch (e: Exception) {
null
}
} else {
null
}
}

suspend fun saveFeed(feed: RemoteSignerFeed, etag: String? = null) {
context.dataStore.edit { preferences ->
preferences[FEED_JSON_KEY] = gson.toJson(feed)
if (etag != null) {
preferences[ETAG_KEY] = etag
}
}
}

suspend fun getEtag(): String? = context.dataStore.data.map { preferences ->
preferences[ETAG_KEY]
}.let { flow ->
// This is a bit of a hack to get the current value from flow
// In a real repo, we'd use firstOrNull() or similar
null // Simplified for now, will improve if needed
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.jksalcedo.librefind.data.local

import com.jksalcedo.librefind.data.remote.SignerApiService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class TrustedRomSignerDb(
private val dataStore: SignerFeedDataStore,
private val apiService: SignerApiService
) {
/**
* Bundled platform signer digests:
* Used to validate AOSP-name system packages (com.android.*).
*/
private val bundledPlatformSigners = setOf(
"c45d15cc0ebf9b91fe03246ad16377fe494a2448802aadc7af632c3197c7e0dc",
"04a4b3425f57c3669a73cd2710c7cbf00d222b6d1202dc474e475e5eb47d5c4c",
"d70e106dfa5f730bf098f4570e7ec80b803e31f33e5678cb5de45d0605d4f692",
"a8318efeeb2f37bd9020aebb576dd952654792f12adc3e5b54fad268ed719825"
)

/**
* Bundled ROM app signer digests:
* Used for ROM-specific system apps (e.g. lineage/voltage/graphene apps).
*/
private val bundledRomAppSigners = setOf(
"035d404701c6d5648fd32fa9f199be75be76c819fab149e40996c87d015f57c9",
"13eb13912637fce93694905057cdc06f7232c8fea6f42d0782d03a10935ea325"
)

/**
* Bundled ROM app prefixes.
*/
private val bundledRomPrefixes = listOf(
"com.lineageos.",
"org.lineageos.",
"org.lineageos.updater",
"org.voltageos.",
"com.crdroid.",
"org.evolutionx.",
"org.aicp.",
"org.pixelexperience.",
"org.derpfest.",
"org.projectelixir.",
"com.risingos.",
"org.havoc.",
"org.arrowos.",
"org.superioros.",
"org.blissroms."
)

val platformSigners: Flow<Set<String>> = dataStore.feedFlow.map { remote ->
bundledPlatformSigners + (remote?.platformSigners?.map { it.lowercase().trim() }?.toSet()
?: emptySet())
}

val romAppSigners: Flow<Set<String>> = dataStore.feedFlow.map { remote ->
bundledRomAppSigners + (remote?.romAppSigners?.map { it.lowercase().trim() }?.toSet()
?: emptySet())
}

val romPrefixes: Flow<List<String>> = dataStore.feedFlow.map { remote ->
(bundledRomPrefixes + (remote?.romPrefixes ?: emptyList())).distinct()
}

suspend fun refreshFeed() {
try {
val url = "https://raw.githubusercontent.com/jksalcedo/librefind/main/signers.json"
val feed = apiService.getSignerFeed(url)
dataStore.saveFeed(feed)
} catch (e: Exception) {
// Fallback to bundled is automatic via Flows
e.printStackTrace()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.jksalcedo.librefind.data.remote

import com.jksalcedo.librefind.data.remote.model.RemoteSignerFeed
import retrofit2.http.GET
import retrofit2.http.Url

interface SignerApiService {
@GET
suspend fun getSignerFeed(@Url url: String): RemoteSignerFeed
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.jksalcedo.librefind.data.remote.model

import com.google.gson.annotations.SerializedName

data class RemoteSignerFeed(
val version: Int,
@SerializedName("updated_at") val updatedAt: String,
@SerializedName("platform_signers") val platformSigners: List<String>,
@SerializedName("rom_app_signers") val romAppSigners: List<String>,
@SerializedName("rom_prefixes") val romPrefixes: List<String>
)
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,32 @@ class DeviceInventoryRepoImpl(
"lenovo", "motorola",
"meizu", "zte", "nubia"
)

private fun isLikelyRomNamespace(packageName: String, prefixes: List<String>): Boolean {
val p = packageName.lowercase(Locale.US)
return prefixes.any { prefix -> p.startsWith(prefix) }
}
}

override suspend fun scanAndClassify(): Flow<List<AppItem>> = flow {
val rawApps = localSource.getRawApps()
val ignoredAppsList = ignoredAppsRepository.getIgnoredPackageNames().first()
val reclassifiedAppsMap = reclassifiedAppsRepository.getReclassifiedApps().first()

val platformSigners = trustedRomSignerDb.platformSigners.first()
val romAppSigners = trustedRomSignerDb.romAppSigners.first()
val romPrefixes = trustedRomSignerDb.romPrefixes.first()

val cacheFresh = cacheRepository.isCacheValid()
var usingStaleCache = false
// var usingStaleCache = false

if (!cacheFresh) {
try {
cacheRepository.refreshCache()
} catch (e: Exception) {
val hasCache = cacheRepository.hasAnyCache()
if (hasCache) {
usingStaleCache = true
// usingStaleCache = true
Log.w(TAG, "Offline/stale mode: using existing cache", e)
} else {
Log.w(TAG, "No cache available; continuing with limited classification", e)
Expand Down Expand Up @@ -127,7 +136,10 @@ class DeviceInventoryRepoImpl(
reclassifiedApps = reclassifiedAppsMap,
proprietaryMap = proprietaryMap,
solutionsSet = solutionsSet,
pendingPackages = pendingPackages
pendingPackages = pendingPackages,
platformSigners = platformSigners,
romAppSigners = romAppSigners,
romPrefixes = romPrefixes
)
}
}.awaitAll()
Expand All @@ -143,7 +155,10 @@ class DeviceInventoryRepoImpl(
reclassifiedApps: Map<String, AppStatus>,
proprietaryMap: Map<String, Boolean>,
solutionsSet: Set<String>,
pendingPackages: Set<String>
pendingPackages: Set<String>,
platformSigners: Set<String>,
romAppSigners: Set<String>,
romPrefixes: List<String>
): AppItem {
val packageName = pkg.packageName
val label = localSource.getAppLabel(packageName)
Expand Down Expand Up @@ -192,7 +207,7 @@ class DeviceInventoryRepoImpl(
}

val digests = SignerUtils.signerSha256Digests(pkg)
val trusted = trustedRomSignerDb.isTrustedSigner(digests)
val trusted = digests.any { it.lowercase() in platformSigners }

if (trusted) {
return createAppItem(
Expand Down Expand Up @@ -220,16 +235,14 @@ class DeviceInventoryRepoImpl(
)
}

if (installer in FOSS_INSTALLERS) {
return createAppItem(
packageName,
label,
AppStatus.FOSS,
installer,
icon,
isUserReclassified = false,
isSystemPackage = isSystem
)
if (isSystem && isLikelyRomNamespace(packageName, romPrefixes)) {
val digests = SignerUtils.signerSha256Digests(pkg)
if (digests.any { it.lowercase() in romAppSigners }) {
return createAppItem(
packageName, label, AppStatus.FOSS, installer, icon,
isUserReclassified = false, isSystemPackage = true
)
}
}

val isKnownSolution = try {
Expand Down Expand Up @@ -268,6 +281,18 @@ class DeviceInventoryRepoImpl(
)
}

if (installer in FOSS_INSTALLERS) {
return createAppItem(
packageName,
label,
AppStatus.FOSS,
installer,
icon,
isUserReclassified = false,
isSystemPackage = isSystem
)
}

if (installer in PROPRIETARY_INSTALLERS) {
return createAppItem(
packageName,
Expand Down
Loading