Skip to content

Commit

Permalink
Persistent storage (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
manhtai authored Jan 18, 2024
1 parent 49c4ec2 commit cb2d880
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 34 deletions.
6 changes: 4 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
}

android {
Expand Down Expand Up @@ -64,8 +65,9 @@ dependencies {
implementation("androidx.compose.material3:material3")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")


implementation("androidx.room:room-runtime:2.6.1")
annotationProcessor("androidx.room:room-compiler:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")

testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
Expand Down
153 changes: 127 additions & 26 deletions app/src/main/java/com/manhtai/whatthefoto/Configuration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.util.Patterns
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
Expand All @@ -25,22 +26,66 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import android.graphics.Color as Colour
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.ContextCompat
import androidx.room.ColumnInfo
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Room
import androidx.room.RoomDatabase
import java.util.Calendar

@Entity
data class Config(
var defaultImage: String = "https://cdn2.thecatapi.com/images/7_rjG2-pc.jpg",
var apiURL: String = "https://api.thecatapi.com/v1/images/search?limit=10",
var imageDelaySeconds: Int = 30,
var imageFadeSeconds: Int = 3,
var sleepFromHour: Int = 19,
var sleepToHour: Int = 8,
val defaultImage: String = "https://cdn2.thecatapi.com/images/7_rjG2-pc.jpg",

@PrimaryKey val id: Int = 1,
@ColumnInfo(name = "api_url") val apiURL: String = "https://api.thecatapi.com/v1/images/search?limit=10",
@ColumnInfo(name = "image_delay_seconds") val imageDelaySeconds: Int = 30,
@ColumnInfo(name = "image_fade_seconds") val imageFadeSeconds: Int = 3,
@ColumnInfo(name = "sleep_from_hour") val sleepFromHour: Int = 19,
@ColumnInfo(name = "sleep_to_hour") val sleepToHour: Int = 8,
@ColumnInfo(name = "background_color") val backgroundColor: String = "#000000",
)

@Dao
interface ConfigDao {
@Query("SELECT * FROM config WHERE id = 1")
fun get(): Config?

@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(conf: Config)
}

@Database(entities = [Config::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun configDao(): ConfigDao

companion object {
@Volatile
private var Instance: AppDatabase? = null

fun getDatabase(context: Context): AppDatabase {
// if the Instance is not null, return it, otherwise create a new database instance.
return Instance ?: synchronized(this) {
Room.databaseBuilder(context, AppDatabase::class.java, "local-database")
.build()
.also { Instance = it }
}
}
}
}


@Composable
fun ConfigurationPopup(
ctx: Context,
Expand All @@ -49,10 +94,17 @@ fun ConfigurationPopup(
oldConf: Config,
) {
var conf by remember { mutableStateOf(oldConf) }
var apiURL by remember { mutableStateOf(oldConf.apiURL) }
var delaySeconds by remember { mutableStateOf(oldConf.imageDelaySeconds.toString()) }
var fadeSeconds by remember { mutableStateOf(oldConf.imageFadeSeconds.toString()) }
var sleepFrom by remember { mutableStateOf(oldConf.sleepFromHour.toString()) }
var sleepTo by remember { mutableStateOf(oldConf.sleepToHour.toString()) }
var bgColor by remember { mutableStateOf(oldConf.backgroundColor) }
var msg by remember { mutableStateOf("") }

Dialog(
onDismissRequest = onDismiss,
properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)
) {
Box(
modifier = Modifier
Expand All @@ -79,8 +131,8 @@ fun ConfigurationPopup(
}

TextField(
value = conf.apiURL,
onValueChange = { conf = conf.copy(apiURL = it) },
value = apiURL,
onValueChange = { apiURL = it },
label = { Text("Image API URL (return [{ url }])") },
modifier = Modifier
.fillMaxWidth()
Expand All @@ -93,21 +145,20 @@ fun ConfigurationPopup(
.fillMaxWidth()
.padding(8.dp)
) {
TextField(value = conf.imageDelaySeconds.toString(),
TextField(value = delaySeconds,
label = { Text(text = "Image delay (seconds)") },
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
onValueChange = {
conf =
conf.copy(imageDelaySeconds = if (it != "" && it.toInt() > 0) it.toInt() else 1)
delaySeconds = it
})

TextField(value = conf.imageFadeSeconds.toString(),
TextField(value = fadeSeconds,
label = { Text(text = "Image fade (seconds)") },
modifier = Modifier
.padding(start = 8.dp),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
onValueChange = {
conf = conf.copy(imageFadeSeconds = if (it != "") it.toInt() else 0)
fadeSeconds = it
})
}

Expand All @@ -116,17 +167,14 @@ fun ConfigurationPopup(
.fillMaxWidth()
.padding(8.dp)
) {
TextField(value = conf.sleepFromHour.toString(),
TextField(value = sleepFrom,
label = { Text(text = "Sleep from hour (0h-24h)") },
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
onValueChange = {
conf = conf.copy(
sleepFromHour =
if (it != "" && it.toInt() in 0..24) it.toInt() else 0
)
sleepFrom = it
})

TextField(value = conf.sleepToHour.toString(),
TextField(value = sleepTo,
label = {
Text(
text = "To hour (0h-24h). Now is " + Calendar.getInstance()
Expand All @@ -136,10 +184,20 @@ fun ConfigurationPopup(
modifier = Modifier.padding(start = 8.dp),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
onValueChange = {
conf = conf.copy(
sleepToHour =
if (it != "" && it.toInt() in 0..24) it.toInt() else 0
)
sleepTo = it
})
}

Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
TextField(value = bgColor,
label = { Text(text = "Background color") },
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text),
onValueChange = {
bgColor = it
})
}

Expand All @@ -156,13 +214,56 @@ fun ConfigurationPopup(
Spacer(modifier = Modifier.width(8.dp))

Button(onClick = {
onSave(conf)
onDismiss()
if (
apiURL != "" && isValidURL(apiURL) &&
delaySeconds != "" &&
fadeSeconds != "" &&
sleepFrom != "" && sleepFrom.toInt() < 24 &&
sleepTo != "" && sleepTo.toInt() < 24 &&
bgColor != "" && isValidColor(bgColor)
) {
conf = conf.copy(
apiURL = apiURL,
imageDelaySeconds = delaySeconds.toInt(),
imageFadeSeconds = fadeSeconds.toInt(),
sleepFromHour = sleepFrom.toInt(),
sleepToHour = sleepTo.toInt(),
backgroundColor = bgColor
)
onSave(conf)
onDismiss()
} else {
msg = "Invalid configuration!"
}
}) {
Text("Save")
}
}

if (msg != "") {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.End
) {
Text(text = msg, color = Color.Red)
}
}
}
}
}
}

fun isValidColor(c: String): Boolean {
return try {
Colour.parseColor(c)
true
} catch (_: Exception) {
false
}
}

fun isValidURL(u: String): Boolean {
return Patterns.WEB_URL.matcher(u).matches()
}
28 changes: 22 additions & 6 deletions app/src/main/java/com/manhtai/whatthefoto/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import android.graphics.Color as Colour
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
Expand Down Expand Up @@ -52,25 +54,34 @@ class MainActivity : ComponentActivity() {

@Composable
fun Main(photoAPI: PhotoApiService = PhotoApi.service) {
val TAG = "Main"
val context = LocalContext.current
val coroutineScope: CoroutineScope = rememberCoroutineScope()

var conf by remember { mutableStateOf(Config()) }
var imageUrl by remember { mutableStateOf(conf.defaultImage) }
var isConfigPopupVisible by remember { mutableStateOf(false) }
var isScreenOn by remember { mutableStateOf(true) }

val TAG = "Main"
val context = LocalContext.current
val coroutineScope: CoroutineScope = rememberCoroutineScope()
var bgColor by remember { mutableIntStateOf(Colour.parseColor(conf.backgroundColor)) }

// Start periodic updates
LaunchedEffect(Unit) {
coroutineScope.launch(Dispatchers.IO) {
val db = AppDatabase.getDatabase(context = context)
val dbConf = db.configDao().get()
if (dbConf == null) {
db.configDao().insert(conf)
} else {
conf = dbConf
}

while (true) {
// Sleep
val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
isScreenOn = if (conf.sleepFromHour < conf.sleepToHour) {
hour < conf.sleepFromHour || hour > conf.sleepToHour
} else {
hour in conf.sleepToHour..conf.sleepFromHour
hour in conf.sleepToHour + 1..<conf.sleepFromHour
}

Log.i(TAG, "Screen is" + if (isScreenOn) " ON." else " OFF.")
Expand Down Expand Up @@ -128,7 +139,7 @@ fun Main(photoAPI: PhotoApiService = PhotoApi.service) {
contentScale = ContentScale.Fit,
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
.background(Color(bgColor))
.pointerInput(Unit) {
detectTapGestures {
isConfigPopupVisible = true
Expand All @@ -144,6 +155,11 @@ fun Main(photoAPI: PhotoApiService = PhotoApi.service) {
onDismiss = { isConfigPopupVisible = false },
onSave = { c ->
conf = c
bgColor = Colour.parseColor(c.backgroundColor)
coroutineScope.launch(Dispatchers.IO) {
val db = AppDatabase.getDatabase(context = context)
db.configDao().insert(conf)
}
},
conf
)
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
plugins {
id("com.android.application") version "8.2.1" apply false
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
id("com.google.devtools.ksp") version "1.9.22-1.0.16" apply false
}

0 comments on commit cb2d880

Please sign in to comment.