Skip to content

Commit

Permalink
chore: refactor UnlaucherApps to com.jkuester package
Browse files Browse the repository at this point in the history
Also add unit tests.
  • Loading branch information
jkuester committed Feb 8, 2025
1 parent b659dd9 commit 592f9b2
Show file tree
Hide file tree
Showing 15 changed files with 521 additions and 264 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ dependencies {
// Test libs
testImplementation(kotlin("test"))
testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
testImplementation("io.kotest:kotest-assertions-core:5.9.1")
testImplementation("io.mockk:mockk-android:1.13.14")
testImplementation("io.mockk:mockk-agent:1.13.14")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
Expand Down Expand Up @@ -40,15 +41,13 @@ abstract class AbstractDataRepository<T>(

fun get(): T = runBlocking { dataFlow.first() }

fun updateAsync(transform: suspend (t: T) -> T) = lifecycleScope.launch {
fun updateAsync(transform: suspend (t: T) -> T) = lifecycleScope.launch(Dispatchers.IO) {
dataStore.updateData(transform)
}
}

abstract class AbstractDataSerializer<T>(
getDefaultInstance: () -> T,
private val parseFrom: (InputStream) -> T
) : Serializer<T> where T : GeneratedMessageLite<T, *> {
abstract class AbstractDataSerializer<T>(getDefaultInstance: () -> T, private val parseFrom: (InputStream) -> T) :
Serializer<T> where T : GeneratedMessageLite<T, *> {
override val defaultValue = getDefaultInstance()

override suspend fun readFrom(input: InputStream): T = try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import com.jkuester.unlauncher.datasource.sharedPrefsMigration as quickButtonSha
import com.jkuester.unlauncher.datastore.proto.CorePreferences
import com.jkuester.unlauncher.datastore.proto.QuickButtonPreferences
import com.jkuester.unlauncher.datastore.proto.UnlauncherApps
import com.sduduzog.slimlauncher.datasource.apps.UnlauncherAppsMigrations
import com.sduduzog.slimlauncher.datasource.apps.UnlauncherAppsSerializer
import com.sduduzog.slimlauncher.datasource.coreprefs.CorePreferencesMigrations
import com.sduduzog.slimlauncher.datasource.coreprefs.CorePreferencesSerializer
import dagger.Module
Expand All @@ -32,7 +30,7 @@ private val Context.quickButtonPreferencesStore: DataStore<QuickButtonPreference
private val Context.unlauncherAppsStore: DataStore<UnlauncherApps> by dataStore(
fileName = "unlauncher_apps.proto",
serializer = UnlauncherAppsSerializer,
produceMigrations = { context -> UnlauncherAppsMigrations().get(context) }
produceMigrations = { listOf(SortAppsMigration) }
)

private val Context.corePreferencesStore: DataStore<CorePreferences> by dataStore(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@file:Suppress("ktlint:standard:filename")

package com.jkuester.unlauncher.datasource

import androidx.datastore.core.DataMigration
import com.jkuester.unlauncher.datastore.proto.UnlauncherApps

object SortAppsMigration : DataMigration<UnlauncherApps> {
private const val VERSION = 1

override suspend fun shouldMigrate(currentData: UnlauncherApps) = currentData.version < VERSION
override suspend fun migrate(currentData: UnlauncherApps) = sortApps(currentData)
.let(setVersion(VERSION))
override suspend fun cleanUp() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package com.jkuester.unlauncher.datasource

import androidx.datastore.core.DataStore
import com.jkuester.unlauncher.datastore.proto.UnlauncherApp
import com.jkuester.unlauncher.datastore.proto.UnlauncherApps
import com.sduduzog.slimlauncher.data.model.App
import com.sduduzog.slimlauncher.models.HomeApp
import dagger.hilt.android.scopes.ActivityScoped
import java.util.Locale
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope

private fun appMatches(unlauncherApp: UnlauncherApp, packageName: String, className: String) =
unlauncherApp.packageName == packageName && unlauncherApp.className == className
private fun appMatches(app: App): (UnlauncherApp) -> Boolean = {
appMatches(it, app.packageName, app.activityName)
}
private fun appMatches(unlauncherApp: UnlauncherApp): (App) -> Boolean = {
appMatches(unlauncherApp, it.packageName, it.activityName)
}
private fun appMatches(homeApp: HomeApp): (UnlauncherApp) -> Boolean = {
appMatches(it, homeApp.packageName, homeApp.activityName)
}
private fun unlauncherAppMatches(app: UnlauncherApp): (UnlauncherApp) -> Boolean = {
appMatches(it, app.packageName, app.className)
}
private fun findUnlauncherApp(unlauncherApps: List<UnlauncherApp>): (HomeApp) -> UnlauncherApp? = { homeApp ->
unlauncherApps.firstOrNull(appMatches(homeApp))
}
private fun unlauncherAppNotFound(unlauncherApps: List<UnlauncherApp>): (UnlauncherApp) -> Boolean = { app ->
unlauncherApps.firstOrNull(unlauncherAppMatches(app)) == null
}
private fun unlauncherAppNotFound(unlauncherApps: UnlauncherApps): (App) -> Boolean = { app ->
unlauncherApps.appsList.firstOrNull(appMatches(app)) == null
}
private fun appNotFound(apps: List<App>): (UnlauncherApp) -> Boolean = { unlauncherApp ->
apps.firstOrNull(appMatches(unlauncherApp)) == null
}

private fun buildUnlauncherApp(app: App): UnlauncherApp = UnlauncherApp
.newBuilder()
.setPackageName(app.packageName)
.setClassName(app.activityName)
.setUserSerial(app.userSerial)
.setDisplayName(app.appName)
.setDisplayInDrawer(true)
.build()

private fun buildUnlauncherApps(unlauncherApps: UnlauncherApps, apps: List<UnlauncherApp>): UnlauncherApps =
unlauncherApps
.toBuilder()
.clearApps()
.addAllApps(apps)
.build()

private fun unlauncherAppOrder(app: UnlauncherApp) = app.displayName.uppercase(Locale.getDefault())

fun setApps(apps: List<App>): (UnlauncherApps) -> UnlauncherApps = { originalApps ->
val appsToAdd = apps
.filter(unlauncherAppNotFound(originalApps))
.map(::buildUnlauncherApp)
val appsToRemove = originalApps.appsList
.filter(appNotFound(apps))
if (appsToAdd.isEmpty() && appsToRemove.isEmpty()) {
originalApps
} else {
originalApps.appsList
.filter { app -> !appsToRemove.contains(app) }
.plus(appsToAdd)
.sortedBy(::unlauncherAppOrder)
.let { buildUnlauncherApps(originalApps, it) }
}
}

private fun setHomeApp(isHomeApp: Boolean): (UnlauncherApp) -> UnlauncherApp = {
it.toBuilder()
.setHomeApp(isHomeApp)
.setDisplayInDrawer(!isHomeApp)
.build()
}

fun setHomeApps(apps: List<HomeApp>): (UnlauncherApps) -> UnlauncherApps = { originalApps ->
val originalHomeApps = originalApps.appsList
.filter { it.homeApp }
.toSet()
val newHomeApps = apps
.mapNotNull(findUnlauncherApp(originalApps.appsList))
.toSet()
val appsToRemove = originalHomeApps
.minus(newHomeApps)
.map(setHomeApp(false))
val appsToAdd = newHomeApps
.minus(originalHomeApps)
.map(setHomeApp(true))
val modifiedApps = appsToRemove.plus(appsToAdd)
if (modifiedApps.isEmpty()) {
originalApps
} else {
originalApps.appsList
.filter(unlauncherAppNotFound(modifiedApps))
.plus(modifiedApps)
.sortedBy(::unlauncherAppOrder)
.let { buildUnlauncherApps(originalApps, it) }
}
}

fun sortApps(unlauncherApps: UnlauncherApps): UnlauncherApps = unlauncherApps
.toBuilder()
.clearApps()
.addAllApps(unlauncherApps.appsList.sortedBy(::unlauncherAppOrder))
.build()

private fun updateApp(
appToUpdate: UnlauncherApp,
update: (UnlauncherApp) -> UnlauncherApp
): (UnlauncherApps) -> UnlauncherApps = { originalApps ->
when (val i = originalApps.appsList.indexOf(appToUpdate)) {
-1 -> originalApps
else -> originalApps
.toBuilder()
.setApps(i, update(appToUpdate))
.build()
}
}

fun setDisplayName(appToUpdate: UnlauncherApp, displayName: String): (UnlauncherApps) -> UnlauncherApps =
{ originalApps ->
updateApp(appToUpdate) { it.toBuilder().setDisplayName(displayName).build() }(originalApps)
.let(::sortApps)
}

fun setDisplayInDrawer(appToUpdate: UnlauncherApp, displayInDrawer: Boolean): (UnlauncherApps) -> UnlauncherApps =
updateApp(appToUpdate) { it.toBuilder().setDisplayInDrawer(displayInDrawer).build() }

fun setVersion(version: Int): (UnlauncherApps) -> UnlauncherApps = { it.toBuilder().setVersion(version).build() }

@ActivityScoped
class UnlauncherAppsRepository @Inject constructor(
unlauncherAppsStore: DataStore<UnlauncherApps>,
lifecycleScope: CoroutineScope
) : AbstractDataRepository<UnlauncherApps>(
unlauncherAppsStore,
lifecycleScope,
UnlauncherApps::getDefaultInstance
)

object UnlauncherAppsSerializer : AbstractDataSerializer<UnlauncherApps>(
UnlauncherApps::getDefaultInstance,
UnlauncherApps::parseFrom
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import android.view.ViewGroup
import android.widget.TextView
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import com.jkuester.unlauncher.datasource.UnlauncherAppsRepository
import com.jkuester.unlauncher.datastore.proto.UnlauncherApp
import com.sduduzog.slimlauncher.R
import com.sduduzog.slimlauncher.datasource.apps.UnlauncherAppsRepository
import com.sduduzog.slimlauncher.datasource.coreprefs.CorePreferencesRepository
import com.sduduzog.slimlauncher.ui.main.HomeFragment
import com.sduduzog.slimlauncher.utils.firstUppercase
Expand All @@ -31,7 +31,7 @@ class AppDrawerAdapter(
private var gravity = 3

init {
unlauncherAppsRepo.liveData().observe(
unlauncherAppsRepo.observe(
lifecycleOwner
) { unlauncherApps ->
apps = unlauncherApps.appsList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,17 @@ import android.view.ViewGroup
import android.widget.CheckBox
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import com.jkuester.unlauncher.datasource.UnlauncherAppsRepository
import com.jkuester.unlauncher.datasource.setDisplayInDrawer
import com.jkuester.unlauncher.datastore.proto.UnlauncherApps
import com.sduduzog.slimlauncher.R
import com.sduduzog.slimlauncher.datasource.apps.UnlauncherAppsRepository

class CustomizeAppDrawerAppsAdapter(
lifecycleOwner: LifecycleOwner,
private val appsRepo: UnlauncherAppsRepository
) : RecyclerView.Adapter<CustomizeAppDrawerAppsAdapter.ViewHolder>() {
class CustomizeAppDrawerAppsAdapter(lifecycleOwner: LifecycleOwner, private val appsRepo: UnlauncherAppsRepository) :
RecyclerView.Adapter<CustomizeAppDrawerAppsAdapter.ViewHolder>() {
private var apps: UnlauncherApps = UnlauncherApps.getDefaultInstance()

init {
appsRepo.liveData().observe(lifecycleOwner, { unlauncherApps ->
appsRepo.observe(lifecycleOwner, { unlauncherApps ->
apps = unlauncherApps
})
}
Expand All @@ -29,7 +28,7 @@ class CustomizeAppDrawerAppsAdapter(
holder.appName.text = item.displayName
holder.appName.isChecked = item.displayInDrawer
holder.itemView.setOnClickListener {
appsRepo.updateDisplayInDrawer(item, holder.appName.isChecked)
appsRepo.updateAsync(setDisplayInDrawer(item, holder.appName.isChecked))
}
}

Expand Down

This file was deleted.

Loading

0 comments on commit 592f9b2

Please sign in to comment.