Skip to content

Commit

Permalink
feat: add a flag Modified to indicate whether icons have been chang…
Browse files Browse the repository at this point in the history
…ed by user
  • Loading branch information
RichardLuo0 committed Jan 30, 2025
1 parent cd69c9b commit 5526822
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 35 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ This module is designed to extend the customization of icon packs throughout the

* Recent screen will use your default launcher unless you use quickswitch. So you will need to select pixel launcher for that to work.
* Pixel launcher saves its icon database in `/data/data/com.google.android.apps.nexuslauncher/databases/app_icons.db`.
* In icon variant, the option `Modified` indicates that you have made changes to the icon variants. If enabled, when the icon pack updates, it will only add new icons instead of modifying existing ones. Note that this could cause issues if any icon entry is missing in the new version!

## Known Issues
* If the launcher is slow to boot or crashes, switch to 'local' mode.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import com.richardluo.globalIconPack.iconPack.IconPackApps
import com.richardluo.globalIconPack.iconPack.loadIconPack
import com.richardluo.globalIconPack.utils.WorldPreference
import com.richardluo.globalIconPack.utils.getFirstRow
import com.richardluo.globalIconPack.utils.getInt
import com.richardluo.globalIconPack.utils.getLong
import com.richardluo.globalIconPack.utils.log
import kotlinx.coroutines.runBlocking

class IconPackDB(private val context: Context, path: String = "iconPack.db") :
SQLiteOpenHelper(context.createDeviceProtectedStorageContext(), path, null, 5) {
SQLiteOpenHelper(context.createDeviceProtectedStorageContext(), path, null, 6) {

override fun onCreate(db: SQLiteDatabase) {}

Expand All @@ -31,17 +32,19 @@ class IconPackDB(private val context: Context, path: String = "iconPack.db") :
}
db.apply {
execSQL(
"CREATE TABLE IF NOT EXISTS 'fallbacks' (pack TEXT PRIMARY KEY NOT NULL, fallback BLOB NOT NULL, updateAt NUMERIC NOT NULL)"
"CREATE TABLE IF NOT EXISTS 'fallbacks' (pack TEXT PRIMARY KEY NOT NULL, fallback BLOB NOT NULL, updateAt NUMERIC NOT NULL, modified INTEGER NOT NULL DEFAULT FALSE)"
)
val packTable = pt(pack)
rawQuery("select DISTINCT updateAt from fallbacks where pack=?", arrayOf(pack)).use { c ->
val info =
runCatching { context.packageManager.getPackageInfo(pack, 0) }
.getOrElse {
return
}
if (info.lastUpdateTime < (c.getFirstRow { it.getLong("updateAt") } ?: 0)) return
}
// Check update time
val lastUpdateTime =
runCatching { context.packageManager.getPackageInfo(pack, 0) }.getOrNull()?.lastUpdateTime
?: return
val modified =
rawQuery("select DISTINCT updateAt, modified from fallbacks where pack=?", arrayOf(pack))
.getFirstRow {
if (lastUpdateTime < it.getLong("updateAt")) return
it.getInt("modified") != 0
} ?: return
// Create tables
execSQL(
"CREATE TABLE IF NOT EXISTS '$packTable' (packageName TEXT NOT NULL, className TEXT NOT NULL, entry BLOB NOT NULL, pack TEXT NOT NULL DEFAULT '')"
Expand All @@ -54,9 +57,9 @@ class IconPackDB(private val context: Context, path: String = "iconPack.db") :
try {
// Load icon pack
loadIconPack(context.packageManager.getResourcesForApplication(pack), pack).let { info ->
delete("'${pt(pack)}'", null, null)
if (!modified) delete("'${pt(pack)}'", null, null)
// Insert icons
insertIcon(db, pack, info.iconEntryMap.toList())
insertIcons(db, pack, info.iconEntryMap.toList())
// Insert fallback
insertFallbackSettings(
db,
Expand All @@ -82,21 +85,58 @@ class IconPackDB(private val context: Context, path: String = "iconPack.db") :
}

fun getFallbackSettings(pack: String) =
readableDatabase.query("fallbacks", null, "pack=?", arrayOf(pack), null, null, null, "1")
readableDatabase.query(
"fallbacks",
arrayOf("fallback"),
"pack=?",
arrayOf(pack),
null,
null,
null,
"1",
)

private fun insertFallbackSettings(db: SQLiteDatabase, pack: String, fs: FallbackSettings) {
db.insertWithOnConflict(
if (
db.query("fallbacks", null, "pack=?", arrayOf(pack), null, null, null, "1").use {
it.count > 0
}
)
db.update(
"fallbacks",
ContentValues().apply {
put("fallback", fs.toByteArray())
put("updateAt", System.currentTimeMillis())
},
"pack=?",
arrayOf(pack),
)
else
db.insert(
"fallbacks",
null,
ContentValues().apply {
put("pack", pack)
put("fallback", fs.toByteArray())
put("updateAt", System.currentTimeMillis())
},
)
}

fun setPackModified(pack: String, modified: Boolean = true) {
writableDatabase.update(
"fallbacks",
null,
ContentValues().apply {
put("pack", pack)
put("fallback", fs.toByteArray())
put("updateAt", System.currentTimeMillis())
},
SQLiteDatabase.CONFLICT_REPLACE,
ContentValues().apply { put("modified", modified) },
"pack=?",
arrayOf(pack),
)
}

fun isPackModified(pack: String) =
readableDatabase
.query("fallbacks", arrayOf("modified"), "pack=?", arrayOf(pack), null, null, null, "1")
.getFirstRow { it.getInt("modified") != 0 } ?: false

private fun getIconExact(pack: String, cn: ComponentName) =
readableDatabase.rawQuery(
"SELECT entry, pack FROM '${pt(pack)}' WHERE packageName=? and className=? LIMIT 1",
Expand All @@ -117,14 +157,14 @@ class IconPackDB(private val context: Context, path: String = "iconPack.db") :
fun getIcon(pack: String, cn: ComponentName, fallback: Boolean = false) =
if (fallback) getIconFallback(pack, cn) else getIconExact(pack, cn)

private fun insertIcon(
private fun insertIcons(
db: SQLiteDatabase,
pack: String,
icons: List<Pair<ComponentName, IconEntry>>,
) {
val insertIcon =
db.compileStatement(
"INSERT OR REPLACE INTO '${pt(pack)}' (packageName, className, entry) VALUES(?, ?, ?) "
"INSERT OR IGNORE INTO '${pt(pack)}' (packageName, className, entry) VALUES(?, ?, ?)"
)
icons.forEach { icon ->
insertIcon.apply {
Expand Down Expand Up @@ -180,6 +220,7 @@ class IconPackDB(private val context: Context, path: String = "iconPack.db") :
},
SQLiteDatabase.CONFLICT_REPLACE,
)
setPackModified(pack)
setTransactionSuccessful()
} catch (e: Exception) {
log(e)
Expand All @@ -190,12 +231,16 @@ class IconPackDB(private val context: Context, path: String = "iconPack.db") :

fun deleteIcon(pack: String, packageName: String) {
writableDatabase.delete("'${pt(pack)}'", "packageName=?", arrayOf(packageName))
setPackModified(pack)
}

fun resetPack(pack: String) {
writableDatabase.update(
"fallbacks",
ContentValues().apply { put("updateAt", 0) },
ContentValues().apply {
put("updateAt", 0)
put("modified", false)
},
"pack=?",
arrayOf(pack),
)
Expand All @@ -210,15 +255,18 @@ class IconPackDB(private val context: Context, path: String = "iconPack.db") :

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.apply {
if (oldVersion == 4 && newVersion == 5) {
foreachPackTable { execSQL("ALTER TABLE '$it' ADD COLUMN pack TEXT NOT NULL DEFAULT ''") }
} else
if (oldVersion >= 4) {
if (oldVersion == 4)
foreachPackTable { execSQL("ALTER TABLE '$it' ADD COLUMN pack TEXT NOT NULL DEFAULT ''") }
execSQL("ALTER TABLE 'fallbacks' ADD COLUMN modified INTEGER NOT NULL DEFAULT FALSE")
} else {
foreachPackTable {
db.execSQL("DROP TABLE '$it'")
db.execSQL("DROP TABLE 'fallbacks'")
execSQL("DROP TABLE '$it'")
execSQL("DROP TABLE 'fallbacks'")
}
WorldPreference.getPrefInApp(context).getString(PrefKey.ICON_PACK, PrefDef.ICON_PACK)?.let {
update(db, it)
}
WorldPreference.getPrefInApp(context).getString(PrefKey.ICON_PACK, PrefDef.ICON_PACK)?.let {
update(db, it)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
Expand Down Expand Up @@ -122,6 +123,14 @@ class IconVariantActivity : ComponentActivity() {
expanded = false
},
)
DropdownMenuItem(
leadingIcon = { Checkbox(viewModel.modified.getValue(), onCheckedChange = null) },
text = { Text(stringResource(R.string.modified)) },
onClick = {
lifecycleScope.launch { viewModel.flipModified() }
expanded = false
},
)
}
},
modifier = Modifier.fillMaxWidth(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import com.richardluo.globalIconPack.utils.getInstance
import com.richardluo.globalIconPack.utils.getString
import kotlin.collections.set
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

Expand All @@ -42,12 +46,19 @@ class IconVariantVM(app: Application) : AndroidViewModel(app) {
private val iconPackAsFallback =
WorldPreference.getPrefInApp(context)
.getBoolean(PrefKey.ICON_PACK_AS_FALLBACK, PrefDef.ICON_PACK_AS_FALLBACK)
var icons = mutableStateMapOf<ComponentName, AppIconInfo>()
private set
val icons = mutableStateMapOf<ComponentName, AppIconInfo>()
private val iconsFlow = snapshotFlow { icons.toMap() }

private val modifiedChangeFlow = MutableSharedFlow<Boolean>(1).apply { tryEmit(false) }
val modified =
combine(iconsFlow, modifiedChangeFlow) { _, _ ->
return@combine withContext(Dispatchers.IO) { iconPackDB.isPackModified(basePack) }
}
.stateIn(viewModelScope, SharingStarted.Lazily, false)

val expandSearchBar = mutableStateOf(false)

val filterAppsVM = FilterAppsVM(snapshotFlow { icons.toMap() })
val filterAppsVM = FilterAppsVM(iconsFlow)
val chooseIconVM = ChooseIconVM(this::basePack, iconCache)

init {
Expand Down Expand Up @@ -97,6 +108,12 @@ class IconVariantVM(app: Application) : AndroidViewModel(app) {
isLoading = false
}

suspend fun flipModified() {
if (basePack.isEmpty()) return
withContext(Dispatchers.IO) { iconPackDB.setPackModified(basePack, !modified.value) }
modifiedChangeFlow.emit(true)
}

suspend fun replaceIcon(icon: VariantIcon) {
val cn = chooseIconVM.selectedApp.value ?: return
isLoading = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,17 @@ val isInMod by lazy {
}
}

fun <T> Cursor.getFirstRow(block: (Cursor) -> T) = this.takeIf { it.moveToFirst() }?.use(block)
inline fun <T> Cursor.getFirstRow(block: (Cursor) -> T) =
this.takeIf { it.moveToFirst() }?.use(block)

fun Cursor.getBlob(name: String) = this.getBlob(getColumnIndexOrThrow(name))

fun Cursor.getLong(name: String) = this.getLong(getColumnIndexOrThrow(name))

fun Cursor.getString(name: String) = this.getString(getColumnIndexOrThrow(name))

fun Cursor.getInt(name: String) = this.getInt(getColumnIndexOrThrow(name))

fun String.ifNotEmpty(block: (String) -> String) = if (isNotEmpty()) block(this) else this

operator fun XmlPullParser.get(key: String): String? = this.getAttributeValue(null, key)
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<string name="allIcons">All icons</string>
<string name="showUserApp">Show user app</string>
<string name="showSystemApp">Show system app</string>
<string name="modified">Modified</string>

<string name="chooseBasePack">Choose a base icon pack</string>
<string name="chooseIconToReplace">Choose the icon to replace</string>
Expand Down

0 comments on commit 5526822

Please sign in to comment.