Initial commit

This commit is contained in:
2025-09-22 19:25:32 -03:00
parent dcd261a2b3
commit e0062445b6
122 changed files with 4728 additions and 19 deletions

View File

@@ -0,0 +1,46 @@
group = "dev.carlosmartino.core"
plugins {
alias(libs.plugins.kotlinDevKit)
alias(libs.plugins.composeDevKit)
}
kotlin {
sourceSets {
commonMain.dependencies {
api(libs.atomicfu)
api(libs.kotlinx.datetime)
api(libs.kotlinx.coroutines.core)
api(libs.kotlinx.serialization.json)
api(libs.koin.core)
api(libs.koin.compose)
api(libs.koin.compose.viewmodel)
api(libs.koin.compose.viewmodel.navigation)
implementation(libs.androidx.security.crypto)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.datastore.preferences.core)
implementation(libs.multiplatform.settings.core)
implementation(libs.multiplatform.settings.noarg)
implementation(libs.multiplatform.settings.coroutines)
implementation(libs.multiplatform.settings.serialization)
implementation(libs.moko.permissions)
implementation(libs.moko.permissions.compose)
implementation(libs.moko.permissions.bluetooth)
implementation(libs.moko.permissions.camera)
implementation(libs.moko.permissions.contacts)
implementation(libs.moko.permissions.gallery)
implementation(libs.moko.permissions.location)
implementation(libs.moko.permissions.microphone)
implementation(libs.moko.permissions.motion)
implementation(libs.moko.permissions.notifications)
implementation(libs.moko.permissions.storage)
}
androidMain.dependencies {
api(libs.kotlinx.coroutines.android)
api(libs.koin.android)
api(libs.multiplatform.settings.core)
}
}
}

View File

@@ -0,0 +1,6 @@
package cl.homelogic.platform.common
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual object Platform {
actual fun getType() = PlatformTypes.Android
}

View File

@@ -0,0 +1,18 @@
package cl.homelogic.platform.common.logging
import android.util.Log
internal actual fun logPlatform(
severity: Severity,
tag: String,
message: String,
) {
when (severity) {
Severity.Debug -> Log.d(tag, message)
Severity.Verbose -> Log.v(tag, message)
Severity.Info -> Log.i(tag, message)
Severity.Warn -> Log.w(tag, message)
Severity.Error -> Log.e(tag, message)
else -> {}
}
}

View File

@@ -0,0 +1,30 @@
package cl.homelogic.platform.common.settings
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.russhwolf.settings.Settings
import com.russhwolf.settings.SharedPreferencesSettings
class AndroidSettingsFactory(
private val context: Context,
) : ISettingsFactory {
override fun createSettings(name: String?): Settings {
val prefName = name ?: "secure_settings"
val masterKey = MasterKey
.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val sharedPreferences = EncryptedSharedPreferences.create(
context,
prefName,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
return SharedPreferencesSettings(sharedPreferences)
}
}

View File

@@ -0,0 +1,11 @@
package cl.homelogic.platform.common
enum class PlatformTypes {
IOS,
Android,
}
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
expect object Platform {
fun getType(): PlatformTypes
}

View File

@@ -0,0 +1,230 @@
package cl.homelogic.platform.common.logging
import kotlinx.atomicfu.locks.SynchronizedObject
import kotlinx.atomicfu.locks.synchronized
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
enum class Severity {
Debug,
Verbose,
Info,
Warn,
Error,
None,
}
enum class TreeRoots {
Navigation,
Koin,
AssetsParser,
DesignSystem,
Settings,
}
private data class LogEntry(
val timestamp: Instant,
val severity: Severity,
val tag: String,
val message: String,
)
private interface LogTree {
val maxEntries: Int
val logs: ArrayDeque<LogEntry>
fun log(
severity: Severity,
tag: String,
message: String,
)
fun getFormattedLogs(): String
}
private class MemoryLogTree(
override val maxEntries: Int,
) : LogTree {
override val logs = ArrayDeque<LogEntry>(maxEntries)
private val logsLock = SynchronizedObject()
override fun log(
severity: Severity,
tag: String,
message: String,
) {
synchronized(logsLock) {
if (logs.size >= maxEntries) {
logs.removeFirst()
}
logs.addLast(LogEntry(Clock.System.now(), severity, tag, message))
}
}
override fun getFormattedLogs(): String =
buildString {
synchronized(logsLock) {
logs.forEach { entry ->
appendLine(
"${entry.timestamp.toLocalDateTime(TimeZone.currentSystemDefault())} " +
"${entry.severity.name.padEnd(7)} " +
"${entry.tag}: ${entry.message}",
)
}
}
}
}
internal expect fun logPlatform(
severity: Severity,
tag: String,
message: String,
)
class Trace {
companion object {
private const val DEFAULT_LOG_MESSAGE = "The message content was empty"
private var _isDebug: Boolean = false
private var _minSeverity: Severity = Severity.None
private var _treeSize: Int = 20
private val _trees = HashMap<String, LogTree>()
fun initialize(isDebug: Boolean) {
_isDebug = isDebug
_minSeverity = if (_isDebug) Severity.Verbose else Severity.None
plantTree(TreeRoots.Navigation)
plantTree(TreeRoots.Koin)
plantTree(TreeRoots.AssetsParser)
plantTree(TreeRoots.DesignSystem)
plantTree(TreeRoots.Settings)
}
fun setLoggingSeverity(severity: Severity) {
_minSeverity = severity
}
fun setTreeSizes(size: Int) {
_treeSize = size
}
private fun plantTree(name: TreeRoots) {
_trees[name.name] = MemoryLogTree(_treeSize)
}
fun plantTree(name: String) {
_trees[name] = MemoryLogTree(_treeSize)
}
fun getTreeContent(name: String): String = _trees[name]?.getFormattedLogs() ?: "Tree '$name' not found"
private fun log(
severity: Severity,
tag: String,
message: String,
) {
if (severity == Severity.None || severity.ordinal < _minSeverity.ordinal) return
logPlatform(severity, "Trace - $tag", message)
_trees.values.forEach { tree ->
tree.log(severity, tag, message)
}
}
fun v(
tag: TreeRoots,
message: String,
) {
v(tag.name, message)
}
fun d(
tag: TreeRoots,
message: String,
) {
d(tag.name, message)
}
fun i(
tag: TreeRoots,
message: String,
) {
i(tag.name, message)
}
fun w(
tag: TreeRoots,
message: String,
) {
w(tag.name, message)
}
fun e(
tag: TreeRoots,
message: String,
) {
e(tag.name, message)
}
fun v(
tag: String,
message: String?,
) {
log(Severity.Verbose, tag, message ?: DEFAULT_LOG_MESSAGE)
}
fun d(
tag: String,
message: String?,
) {
log(Severity.Debug, tag, message ?: DEFAULT_LOG_MESSAGE)
}
fun i(
tag: String,
message: String?,
) {
log(Severity.Info, tag, message ?: DEFAULT_LOG_MESSAGE)
}
fun w(
tag: String,
message: String?,
) {
log(Severity.Warn, tag, message ?: DEFAULT_LOG_MESSAGE)
}
fun e(
tag: String,
message: String?,
) {
log(Severity.Error, tag, message ?: DEFAULT_LOG_MESSAGE)
}
fun e(
tag: String,
message: String,
throwable: Throwable,
) {
log(
Severity.Error,
tag,
(message + "\n" + throwable.message + "\n" + throwable.stackTraceToString()),
)
}
fun e(
tag: String,
message: String,
exception: Exception,
) {
log(
Severity.Error,
tag,
(message + "\n" + exception.message + "\n" + exception.stackTraceToString()),
)
}
}
}

View File

@@ -0,0 +1,24 @@
package cl.homelogic.platform.common.logging
import org.koin.core.logger.Level
import org.koin.core.logger.Logger
import org.koin.core.logger.MESSAGE
class TraceKoinLogger : Logger() {
override fun display(
level: Level,
msg: MESSAGE,
) {
when (level) {
Level.DEBUG -> Trace.d(DEFAULT_TAG, msg)
Level.INFO -> Trace.i(DEFAULT_TAG, msg)
Level.WARNING -> Trace.w(DEFAULT_TAG, msg)
Level.ERROR -> Trace.e(DEFAULT_TAG, msg)
Level.NONE -> {}
}
}
companion object {
const val DEFAULT_TAG = "Koin"
}
}

View File

@@ -0,0 +1,77 @@
package cl.homelogic.platform.common.permissions
import cl.homelogic.platform.common.permissions.PermissionStatus.Denied
import cl.homelogic.platform.common.permissions.PermissionStatus.Granted
import cl.homelogic.platform.common.permissions.PermissionStatus.NotGranted
import dev.icerock.moko.permissions.Permission
import dev.icerock.moko.permissions.PermissionState
import dev.icerock.moko.permissions.bluetooth.BluetoothAdvertisePermission
import dev.icerock.moko.permissions.bluetooth.BluetoothConnectPermission
import dev.icerock.moko.permissions.bluetooth.BluetoothLEPermission
import dev.icerock.moko.permissions.bluetooth.BluetoothScanPermission
import dev.icerock.moko.permissions.camera.CameraPermission
import dev.icerock.moko.permissions.contacts.ContactPermission
import dev.icerock.moko.permissions.gallery.GalleryPermission
import dev.icerock.moko.permissions.location.BackgroundLocationPermission
import dev.icerock.moko.permissions.location.CoarseLocationPermission
import dev.icerock.moko.permissions.location.LocationPermission
import dev.icerock.moko.permissions.microphone.RecordAudioPermission
import dev.icerock.moko.permissions.motion.MotionPermission
import dev.icerock.moko.permissions.notifications.RemoteNotificationPermission
import dev.icerock.moko.permissions.storage.StoragePermission
import dev.icerock.moko.permissions.storage.WriteStoragePermission
enum class PermissionType {
CAMERA,
GALLERY,
STORAGE,
WRITE_STORAGE,
LOCATION,
COARSE_LOCATION,
BACKGROUND_LOCATION,
BLUETOOTH_LE,
REMOTE_NOTIFICATION,
RECORD_AUDIO,
BLUETOOTH_SCAN,
BLUETOOTH_ADVERTISE,
BLUETOOTH_CONNECT,
CONTACTS,
MOTION,
;
companion object {
fun PermissionType.toMokoPermission() =
when (this) {
CAMERA -> CameraPermission
GALLERY -> GalleryPermission
STORAGE -> StoragePermission
WRITE_STORAGE -> WriteStoragePermission
LOCATION -> LocationPermission
COARSE_LOCATION -> CoarseLocationPermission
BACKGROUND_LOCATION -> BackgroundLocationPermission
BLUETOOTH_LE -> BluetoothLEPermission
REMOTE_NOTIFICATION -> RemoteNotificationPermission
RECORD_AUDIO -> RecordAudioPermission
BLUETOOTH_SCAN -> BluetoothScanPermission
BLUETOOTH_ADVERTISE -> BluetoothAdvertisePermission
BLUETOOTH_CONNECT -> BluetoothConnectPermission
CONTACTS -> ContactPermission
MOTION -> MotionPermission
}
}
}
fun PermissionState.toPermissionStatus() =
when (this) {
PermissionState.NotDetermined -> NotGranted
PermissionState.NotGranted -> NotGranted
PermissionState.Granted -> Granted
PermissionState.Denied -> Denied
PermissionState.DeniedAlways -> Denied
}
enum class PermissionStatus {
NotGranted,
Granted,
Denied,
}

View File

@@ -0,0 +1,74 @@
package cl.homelogic.platform.common.permissions.composables
import androidx.compose.foundation.layout.LayoutScopeMarker
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import cl.homelogic.platform.common.permissions.PermissionStatus
import cl.homelogic.platform.common.permissions.PermissionType
import cl.homelogic.platform.common.permissions.PermissionType.Companion.toMokoPermission
import cl.homelogic.platform.common.permissions.toPermissionStatus
import dev.icerock.moko.permissions.DeniedAlwaysException
import dev.icerock.moko.permissions.DeniedException
import dev.icerock.moko.permissions.PermissionsController
import dev.icerock.moko.permissions.RequestCanceledException
import dev.icerock.moko.permissions.compose.PermissionsControllerFactory
import dev.icerock.moko.permissions.compose.rememberPermissionsControllerFactory
@Composable
fun PermissionSystem(
content: @Composable PermissionsScope.() -> Unit,
) {
val factory: PermissionsControllerFactory = rememberPermissionsControllerFactory()
val controller: PermissionsController = remember(factory) {
factory.createPermissionsController()
}
PermissionsScopeInstance(controller).content()
}
@LayoutScopeMarker
@Immutable
interface PermissionsScope {
suspend fun requestPermission(permission: PermissionType): PermissionStatus
suspend fun isPermissionGranted(permission: PermissionType): Boolean
suspend fun getPermissionState(permission: PermissionType): PermissionStatus
fun openAppSettings()
}
internal class PermissionsScopeInstance(
private val permissionsController: PermissionsController,
) : PermissionsScope {
override suspend fun requestPermission(permission: PermissionType): PermissionStatus {
try {
permissionsController.providePermission(permission.toMokoPermission())
return PermissionStatus.Granted
} catch (deniedAlways: DeniedAlwaysException) {
return PermissionStatus.Denied
} catch (denied: DeniedException) {
return PermissionStatus.Denied
} catch (canceled: RequestCanceledException) {
return PermissionStatus.NotGranted
} catch (exception: Exception) {
return PermissionStatus.NotGranted
}
}
override suspend fun isPermissionGranted(permission: PermissionType): Boolean =
permissionsController.isPermissionGranted(
permission.toMokoPermission(),
)
override suspend fun getPermissionState(permission: PermissionType): PermissionStatus =
permissionsController
.getPermissionState(
permission.toMokoPermission(),
).toPermissionStatus()
override fun openAppSettings() {
permissionsController.openAppSettings()
}
}

View File

@@ -0,0 +1,57 @@
package cl.homelogic.platform.common.settings
import cl.homelogic.platform.common.logging.Trace
import cl.homelogic.platform.common.logging.TreeRoots
import com.russhwolf.settings.Settings
interface ISettingsSource {
fun <T> get(
key: PreferenceKey<T>,
defaultValue: T,
): T
fun <T> set(
key: PreferenceKey<T>,
value: T,
)
fun <T> remove(key: PreferenceKey<T>)
fun clear()
}
class SettingsSource(
private val settings: Settings,
) : ISettingsSource {
// TODO: add fuction to check if key exists
override fun <T> get(
key: PreferenceKey<T>,
defaultValue: T,
): T {
Trace.d(TreeRoots.Settings, "Retrieving setting: ${key.key}")
return key.getValue(settings, defaultValue)
}
override fun <T> set(
key: PreferenceKey<T>,
value: T,
) {
Trace.d(TreeRoots.Settings, "Storing setting: ${key.key} : $value")
key.setValue(settings, value)
}
override fun <T> remove(key: PreferenceKey<T>) {
Trace.d(TreeRoots.Settings, "Removing setting: ${key.key}")
settings.remove(key.key)
}
override fun clear() {
Trace.d(TreeRoots.Settings, "Deleting all settings")
settings.clear()
}
}
interface ISettingsFactory {
fun createSettings(name: String? = null): Settings
}

View File

@@ -0,0 +1,101 @@
package cl.homelogic.platform.common.settings
import com.russhwolf.settings.Settings
sealed class PreferenceKey<T>(
val key: String,
) {
abstract fun getValue(
settings: Settings,
defaultValue: T,
): T
abstract fun setValue(
settings: Settings,
value: T,
)
class StringKey(
key: String,
) : PreferenceKey<String>(key) {
override fun getValue(
settings: Settings,
defaultValue: String,
): String = settings.getString(key, defaultValue)
override fun setValue(
settings: Settings,
value: String,
) = settings.putString(key, value)
}
class IntKey(
key: String,
) : PreferenceKey<Int>(key) {
override fun getValue(
settings: Settings,
defaultValue: Int,
): Int = settings.getInt(key, defaultValue)
override fun setValue(
settings: Settings,
value: Int,
) = settings.putInt(key, value)
}
class LongKey(
key: String,
) : PreferenceKey<Long>(key) {
override fun getValue(
settings: Settings,
defaultValue: Long,
): Long = settings.getLong(key, defaultValue)
override fun setValue(
settings: Settings,
value: Long,
) = settings.putLong(key, value)
}
class FloatKey(
key: String,
) : PreferenceKey<Float>(key) {
override fun getValue(
settings: Settings,
defaultValue: Float,
): Float = settings.getFloat(key, defaultValue)
override fun setValue(
settings: Settings,
value: Float,
) = settings.putFloat(key, value)
}
class DoubleKey(
key: String,
) : PreferenceKey<Double>(key) {
override fun getValue(
settings: Settings,
defaultValue: Double,
): Double = settings.getDouble(key, defaultValue)
override fun setValue(
settings: Settings,
value: Double,
) = settings.putDouble(key, value)
}
class BooleanKey(
key: String,
) : PreferenceKey<Boolean>(key) {
override fun getValue(
settings: Settings,
defaultValue: Boolean,
): Boolean = settings.getBoolean(key, defaultValue)
override fun setValue(
settings: Settings,
value: Boolean,
) = settings.putBoolean(key, value)
}
}

View File

@@ -0,0 +1,6 @@
package cl.homelogic.platform.common
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual object Platform {
actual fun getType() = PlatformTypes.IOS
}

View File

@@ -0,0 +1,12 @@
package cl.homelogic.platform.common.logging
import platform.Foundation.NSLog
internal actual fun logPlatform(
severity: Severity,
tag: String,
message: String,
) {
if (severity == Severity.None) return
NSLog("Trace - $tag [${severity.name}]: $message")
}

View File

@@ -0,0 +1,16 @@
package cl.homelogic.platform.common.settings
import com.russhwolf.settings.NSUserDefaultsSettings
import com.russhwolf.settings.Settings
import platform.Foundation.NSUserDefaults
class IOSSettingsFactory : ISettingsFactory {
override fun createSettings(name: String?): Settings {
val userDefaults = if (name != null) {
NSUserDefaults(suiteName = name)
} else {
NSUserDefaults.standardUserDefaults
}
return NSUserDefaultsSettings(userDefaults)
}
}