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
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ dependencies {
// Dexter
implementation "com.karumi:dexter:$dexter_version"

// WorkManager
implementation "androidx.work:work-runtime-ktx:2.9.0"

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.activity:activity:1.9.1'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.turingheights.wally.commons.data.local.daos

import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import com.turingheights.wally.commons.data.local.PhotoDatabase
import com.turingheights.wally.commons.data.local.entities.CacheMetadataEntity
import com.turingheights.wally.commons.models.Photo
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class CachedPhotoDaoTest {

private lateinit var database: PhotoDatabase
private lateinit var dao: CachedPhotoDao

private val mockPhoto = Photo(
id = 1,
previewURL = "preview",
fullHDURL = "full",
imageURL = "image",
previewHeight = 100,
previewWidth = 100,
largeImageURL = "large",
webformatURL = "web",
webformatWidth = 100,
webformatHeight = 100
)

@Before
fun setup() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
PhotoDatabase::class.java
).allowMainThreadQueries().build()
dao = database.cachedPhotoDao()
}

@After
fun tearDown() {
database.close()
}

@Test
fun saveAndGetCachedPhotos() = runTest {
val metadata = CacheMetadataEntity(
query = "nature",
category = "all",
safeSearch = true,
orientation = "all",
imageType = "all",
order = "popular"
)

dao.saveCache(metadata, listOf(mockPhoto))

val cachedPhotos = dao.getCachedPhotos(
query = "nature",
category = "all",
safeSearch = true,
orientation = "all",
imageType = "all",
order = "popular"
)

assertThat(cachedPhotos).hasSize(1)
assertThat(cachedPhotos[0].id).isEqualTo(mockPhoto.id)
}

@Test
fun getCachedPhotos_returnsEmpty_whenNoMatch() = runTest {
val metadata = CacheMetadataEntity(
query = "nature",
category = "all",
safeSearch = true,
orientation = "all",
imageType = "all",
order = "popular"
)

dao.saveCache(metadata, listOf(mockPhoto))

val cachedPhotos = dao.getCachedPhotos(
query = "different",
category = "all",
safeSearch = true,
orientation = "all",
imageType = "all",
order = "popular"
)

assertThat(cachedPhotos).isEmpty()
}
}
20 changes: 19 additions & 1 deletion app/src/main/java/com/turingheights/wally/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import dagger.hilt.android.AndroidEntryPoint
import com.turingheights.wally.commons.utils.CacheCleanupWorker
import com.turingheights.wally.databinding.ActivityMainBinding
import java.util.concurrent.TimeUnit

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
Expand All @@ -23,5 +28,18 @@ class MainActivity : AppCompatActivity() {

navController =
(supportFragmentManager.findFragmentById(viewBinding.navHost.id) as NavHostFragment).navController

scheduleCacheCleanup()
}

private fun scheduleCacheCleanup() {
val cleanupRequest = PeriodicWorkRequestBuilder<CacheCleanupWorker>(1, TimeUnit.DAYS)
.build()

WorkManager.getInstance(applicationContext).enqueueUniquePeriodicWork(
"CacheCleanup",
ExistingPeriodicWorkPolicy.KEEP,
cleanupRequest
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,36 @@ import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.turingheights.wally.commons.data.local.daos.CachedPhotoDao
import com.turingheights.wally.commons.data.local.daos.PhotoDao
import com.turingheights.wally.commons.data.local.entities.CacheMetadataEntity
import com.turingheights.wally.commons.data.local.entities.CachedPhotoEntity
import com.turingheights.wally.commons.data.local.entities.PhotoEntity

@Database(entities = [PhotoEntity::class], version = 1)
@Database(
entities = [
PhotoEntity::class,
CachedPhotoEntity::class,
CacheMetadataEntity::class
],
version = 2
)
abstract class PhotoDatabase: RoomDatabase() {

abstract fun photoDao(): PhotoDao
abstract fun cachedPhotoDao(): CachedPhotoDao

companion object {
private var INSTANCE: PhotoDatabase? = null
fun getInstance(context: Context): PhotoDatabase {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context, PhotoDatabase::class.java, "photo.db").build()
INSTANCE = Room.databaseBuilder(context, PhotoDatabase::class.java, "photo.db")
.fallbackToDestructiveMigration()
.build()
}
return INSTANCE!!
}

}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.turingheights.wally.commons.data.local.daos

import androidx.room.*
import com.turingheights.wally.commons.data.local.entities.CacheMetadataEntity
import com.turingheights.wally.commons.data.local.entities.CachedPhotoEntity
import com.turingheights.wally.commons.models.Photo

@Dao
interface CachedPhotoDao {

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMetadata(metadata: CacheMetadataEntity): Long

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCachedPhotos(photos: List<CachedPhotoEntity>)

@Query("""
SELECT * FROM cache_metadata
WHERE `query` IS :query
AND category IS :category
AND safeSearch = :safeSearch
AND orientation = :orientation
AND imageType = :imageType
AND `order` = :order
ORDER BY timestamp DESC LIMIT 1
""")
suspend fun getLatestMetadata(
query: String?,
category: String?,
safeSearch: Boolean,
orientation: String,
imageType: String,
order: String
): CacheMetadataEntity?

@Query("SELECT * FROM cached_photos WHERE cacheMetadataId = :metadataId")
suspend fun getPhotosForMetadata(metadataId: Long): List<CachedPhotoEntity>

@Transaction
suspend fun saveCache(metadata: CacheMetadataEntity, photos: List<Photo>) {
// Optional: clear old cache for these params first
deleteOldCache(metadata.query, metadata.category, metadata.safeSearch, metadata.orientation, metadata.imageType, metadata.order)

val id = insertMetadata(metadata)
val entities = photos.map { CachedPhotoEntity.fromPhoto(it, id) }
insertCachedPhotos(entities)
}

@Query("""
DELETE FROM cache_metadata
WHERE `query` IS :query
AND category IS :category
AND safeSearch = :safeSearch
AND orientation = :orientation
AND imageType = :imageType
AND `order` = :order
""")
suspend fun deleteOldCache(
query: String?,
category: String?,
safeSearch: Boolean,
orientation: String,
imageType: String,
order: String
)

// Helper to get photos directly if cache exists
@Transaction
suspend fun getCachedPhotos(
query: String?,
category: String?,
safeSearch: Boolean,
orientation: String,
imageType: String,
order: String
): List<CachedPhotoEntity> {
val metadata = getLatestMetadata(query, category, safeSearch, orientation, imageType, order)
return metadata?.let { getPhotosForMetadata(it.id) } ?: emptyList()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.turingheights.wally.commons.data.local.entities

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "cache_metadata")
data class CacheMetadataEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val query: String?,
val category: String?,
val safeSearch: Boolean,
val orientation: String,
val imageType: String,
val order: String,
val timestamp: Long = System.currentTimeMillis()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.turingheights.wally.commons.data.local.entities

import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import com.turingheights.wally.commons.models.Photo

@Entity(
tableName = "cached_photos",
foreignKeys = [
ForeignKey(
entity = CacheMetadataEntity::class,
parentColumns = ["id"],
childColumns = ["cacheMetadataId"],
onDelete = ForeignKey.CASCADE
)
],
indices = [Index(value = ["cacheMetadataId"])]
)
data class CachedPhotoEntity(
@PrimaryKey(autoGenerate = true)
val cacheEntryId: Long = 0,
val cacheMetadataId: Long,
val id: Int, // The original photo ID from API
val previewURL: String,
val fullHDURL: String?,
val imageURL: String?,
val previewHeight: Int,
val previewWidth: Int,
val largeImageURL: String,
val webformatURL: String,
val webformatWidth: Int,
val webformatHeight: Int
) {
companion object {
fun fromPhoto(photo: Photo, cacheMetadataId: Long) = CachedPhotoEntity(
cacheMetadataId = cacheMetadataId,
id = photo.id,
previewURL = photo.previewURL,
fullHDURL = photo.fullHDURL,
imageURL = photo.imageURL,
previewHeight = photo.previewHeight,
previewWidth = photo.previewWidth,
largeImageURL = photo.largeImageURL,
webformatURL = photo.webformatURL,
webformatWidth = photo.webformatWidth,
webformatHeight = photo.webformatHeight,
)

fun toPhoto(entity: CachedPhotoEntity) = Photo(
id = entity.id,
previewURL = entity.previewURL,
fullHDURL = entity.fullHDURL,
imageURL = entity.imageURL,
previewHeight = entity.previewHeight,
previewWidth = entity.previewWidth,
largeImageURL = entity.largeImageURL,
webformatURL = entity.webformatURL,
webformatWidth = entity.webformatWidth,
webformatHeight = entity.webformatHeight,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ class DataModule {
return PhotoDatabase.getInstance(context)
}

@Provides
fun providesCachedPhotoDao(database: PhotoDatabase) = database.cachedPhotoDao()

}
Loading