diff --git a/app/build.gradle b/app/build.gradle index 3209caa..145e179 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,6 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'androidx.navigation.safeargs' @@ -39,6 +41,9 @@ dependencies { implementation "androidx.navigation:navigation-fragment-ktx:$versions.navigation" implementation "androidx.navigation:navigation-ui-ktx:$versions.navigation" + implementation "androidx.room:room-runtime:$versions.room" + kapt "androidx.room:room-compiler:$versions.room" + // https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-module-kotlin implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson_module_kotlin" diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/VTUCSLabApplication.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/VTUCSLabApplication.kt index 93685ba..d390c02 100644 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/VTUCSLabApplication.kt +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/VTUCSLabApplication.kt @@ -1,19 +1,42 @@ package com.nagpal.shivam.vtucslab import androidx.multidex.MultiDexApplication +import androidx.room.Room import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.nagpal.shivam.vtucslab.data.local.AppDatabase import com.nagpal.shivam.vtucslab.repositories.VtuCsLabRepository import com.nagpal.shivam.vtucslab.repositories.VtuCsLabRepositoryImpl import com.nagpal.shivam.vtucslab.services.VtuCsLabService +import com.nagpal.shivam.vtucslab.utilities.Constants.VTU_CS_LAB +import com.nagpal.shivam.vtucslab.utilities.StaticMethods class VTUCSLabApplication : MultiDexApplication() { - val vtuCsLabRepository: VtuCsLabRepository = - VtuCsLabRepositoryImpl(this, VtuCsLabService.instance) + private lateinit var _db: AppDatabase + val db: AppDatabase + get() = _db + + private lateinit var _vtuCsLabRepository: VtuCsLabRepository + val vtuCsLabRepository: VtuCsLabRepository + get() = _vtuCsLabRepository override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false) } + + _db = Room.databaseBuilder( + applicationContext, + AppDatabase::class.java, + VTU_CS_LAB + ).build() + + val vtuCsLabService = VtuCsLabService.instance + _vtuCsLabRepository = VtuCsLabRepositoryImpl( + this, + vtuCsLabService, + _db.labResponseDao(), + StaticMethods.jsonMapper + ) } } diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/AppDatabase.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/AppDatabase.kt new file mode 100644 index 0000000..2a39579 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/AppDatabase.kt @@ -0,0 +1,11 @@ +package com.nagpal.shivam.vtucslab.data.local + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters + +@Database(entities = [LabResponse::class], version = 1) +@TypeConverters(Converters::class) +abstract class AppDatabase : RoomDatabase() { + abstract fun labResponseDao(): LabResponseDao +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/Converters.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/Converters.kt new file mode 100644 index 0000000..ee97e52 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/Converters.kt @@ -0,0 +1,16 @@ +package com.nagpal.shivam.vtucslab.data.local + +import androidx.room.TypeConverter +import java.util.* + +class Converters { + @TypeConverter + fun dateFromTimestamp(value: Long?): Date? { + return value?.let { Date(it) } + } + + @TypeConverter + fun dateToTimestamp(date: Date?): Long? { + return date?.time + } +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/DBConstants.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/DBConstants.kt new file mode 100644 index 0000000..57c0b6e --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/DBConstants.kt @@ -0,0 +1,12 @@ +package com.nagpal.shivam.vtucslab.data.local + +object Tables { + const val LAB_RESPONSE = "lab_response" +} + +object LabResponseAttributes { + const val URL = "url" + const val RESPONSE = "response" + const val RESPONSE_TYPE = "response_type" + const val FETCHED_AT = "fetched_at" +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponse.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponse.kt new file mode 100644 index 0000000..f0a9ea4 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponse.kt @@ -0,0 +1,19 @@ +package com.nagpal.shivam.vtucslab.data.local + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.nagpal.shivam.vtucslab.data.local.LabResponseAttributes.FETCHED_AT +import com.nagpal.shivam.vtucslab.data.local.LabResponseAttributes.RESPONSE +import com.nagpal.shivam.vtucslab.data.local.LabResponseAttributes.RESPONSE_TYPE +import com.nagpal.shivam.vtucslab.data.local.LabResponseAttributes.URL +import com.nagpal.shivam.vtucslab.data.local.Tables.LAB_RESPONSE +import java.util.* + +@Entity(LAB_RESPONSE) +data class LabResponse( + @PrimaryKey @ColumnInfo(name = URL) val url: String, + @ColumnInfo(name = RESPONSE) val response: String, + @ColumnInfo(name = RESPONSE_TYPE) val responseType: LabResponseType, + @ColumnInfo(name = FETCHED_AT) val fetchedAt: Date, +) diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponseDao.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponseDao.kt new file mode 100644 index 0000000..f8d7b74 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponseDao.kt @@ -0,0 +1,16 @@ +package com.nagpal.shivam.vtucslab.data.local + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Upsert +import com.nagpal.shivam.vtucslab.data.local.LabResponseAttributes.URL +import com.nagpal.shivam.vtucslab.data.local.Tables.LAB_RESPONSE + +@Dao +interface LabResponseDao { + @Upsert + fun upsert(labResponse: LabResponse) + + @Query("SELECT * FROM $LAB_RESPONSE where $URL = :url") + fun findByUrl(url: String): LabResponse? +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponseType.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponseType.kt new file mode 100644 index 0000000..90347b9 --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/data/local/LabResponseType.kt @@ -0,0 +1,7 @@ +package com.nagpal.shivam.vtucslab.data.local + +enum class LabResponseType { + LABORATORY, + EXPERIMENT, + CONTENT, +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/repositories/VtuCsLabRepositoryImpl.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/repositories/VtuCsLabRepositoryImpl.kt index 67fcbbb..897129c 100644 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/repositories/VtuCsLabRepositoryImpl.kt +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/repositories/VtuCsLabRepositoryImpl.kt @@ -1,65 +1,150 @@ package com.nagpal.shivam.vtucslab.repositories import android.app.Application +import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.databind.json.JsonMapper import com.nagpal.shivam.vtucslab.core.ErrorType import com.nagpal.shivam.vtucslab.core.Resource +import com.nagpal.shivam.vtucslab.data.local.LabResponse +import com.nagpal.shivam.vtucslab.data.local.LabResponseDao +import com.nagpal.shivam.vtucslab.data.local.LabResponseType import com.nagpal.shivam.vtucslab.models.LaboratoryExperimentResponse import com.nagpal.shivam.vtucslab.models.LaboratoryResponse import com.nagpal.shivam.vtucslab.retrofit.ApiResult import com.nagpal.shivam.vtucslab.services.VtuCsLabService +import com.nagpal.shivam.vtucslab.utilities.Configurations import com.nagpal.shivam.vtucslab.utilities.NetworkUtils import com.nagpal.shivam.vtucslab.utilities.StaticMethods import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow +import java.util.Date private val LOG_TAG: String = VtuCsLabRepositoryImpl::class.java.name class VtuCsLabRepositoryImpl( private val application: Application, - private val vtuCsLabService: VtuCsLabService + private val vtuCsLabService: VtuCsLabService, + private val labResponseDao: LabResponseDao, + private val jsonMapper: JsonMapper, ) : VtuCsLabRepository { override fun fetchLaboratories(url: String): Flow> = flow { - fetch(url) { - vtuCsLabService.getLaboratoryResponse(it) + fetch( + flow = this, + url, + LabResponseType.LABORATORY, + vtuCsLabService::getLaboratoryResponse, + { data -> jsonMapper.writeValueAsString(data) } + ) { stringContent -> + jsonMapper.readValue( + stringContent, + LaboratoryResponse::class.java + ) } } override fun fetchExperiments(url: String): Flow> = flow { - fetch(url) { - vtuCsLabService.getLaboratoryExperimentsResponse(it) + fetch( + flow = this, + url, + LabResponseType.EXPERIMENT, + vtuCsLabService::getLaboratoryExperimentsResponse, + { data -> jsonMapper.writeValueAsString(data) } + ) { stringContent -> + jsonMapper.readValue( + stringContent, + LaboratoryExperimentResponse::class.java + ) } } override fun fetchContent(url: String): Flow> = flow { - fetch(url) { - vtuCsLabService.fetchRawResponse(it) - } + fetch( + flow = this, + url, + LabResponseType.CONTENT, + vtuCsLabService::fetchRawResponse, + { stringContent -> stringContent } + ) { stringContent -> stringContent } } - private suspend fun FlowCollector>.fetch( + private suspend fun fetch( + flow: FlowCollector>, url: String, - executable: suspend (String) -> ApiResult + labResponseType: LabResponseType, + fetchFromNetwork: suspend (String) -> ApiResult, + encodeToString: (D) -> String, + decodeFromString: (String) -> D, ) { - emit(Resource.Loading()) + flow.emit(Resource.Loading()) + + val labResponse = labResponseDao.findByUrl(url) + var foundInDB = false + labResponse?.let { + try { + flow.emit(Resource.Success(decodeFromString.invoke(it.response))) + foundInDB = true + if (it.fetchedAt.after( + StaticMethods.getCurrentDateMinusSeconds(Configurations.RESPONSE_FRESHNESS_TIME) + ) + ) { + return + } + } catch (_: JsonParseException) { + } + } + if (!NetworkUtils.isNetworkConnected(application)) { - emit(Resource.Error(ErrorType.NoActiveInternetConnection)) + emitNetworkErrors( + flow, + foundInDB, + Resource.Error(ErrorType.NoActiveInternetConnection), + ) return } - when (val apiResult = executable.invoke(url)) { + when (val apiResult = fetchFromNetwork.invoke(url)) { is ApiResult.ApiSuccess -> { - emit(Resource.Success(apiResult.data)) + val data = apiResult.data + labResponseDao.upsert( + LabResponse( + url, + encodeToString(data), + labResponseType, + Date(), + ) + ) + flow.emit(Resource.Success(data)) } + is ApiResult.ApiError -> { StaticMethods.logNetworkResultError(LOG_TAG, url, apiResult.code, apiResult.message) - emit(Resource.Error(ErrorType.SomeErrorOccurred)) + emitNetworkErrors( + flow, + foundInDB, + Resource.Error(ErrorType.SomeErrorOccurred), + ) } + is ApiResult.ApiException -> { StaticMethods.logNetworkResultException(LOG_TAG, url, apiResult.throwable) - emit(Resource.Error(ErrorType.SomeErrorOccurred)) + emitNetworkErrors( + flow, + foundInDB, + Resource.Error(ErrorType.SomeErrorOccurred), + ) } } } + + private suspend fun emitNetworkErrors( + flow: FlowCollector>, + foundInDB: Boolean, + errorResource: Resource.Error + ) { + if (!foundInDB) { + flow.emit(errorResource) + } + } } diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Configurations.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Configurations.kt new file mode 100644 index 0000000..60f0e7a --- /dev/null +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Configurations.kt @@ -0,0 +1,5 @@ +package com.nagpal.shivam.vtucslab.utilities + +object Configurations { + const val RESPONSE_FRESHNESS_TIME = 3 * 60 * 60 +} diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Constants.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Constants.kt index a6e4506..378bab7 100644 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Constants.kt +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/Constants.kt @@ -8,4 +8,5 @@ object Constants { "https://raw.githubusercontent.com/vtucs/Index_v3/master/Index_v3.json" const val LABEL_CODE = "Code" const val GITHUB_RAW_CONTENT = "github_raw_content" + const val VTU_CS_LAB = "vtu_cs_lab" } diff --git a/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/StaticMethods.kt b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/StaticMethods.kt index b2c685e..4288884 100644 --- a/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/StaticMethods.kt +++ b/app/src/main/java/com/nagpal/shivam/vtucslab/utilities/StaticMethods.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.nagpal.shivam.vtucslab.models.LaboratoryExperimentResponse import com.nagpal.shivam.vtucslab.models.LaboratoryResponse +import java.util.* object StaticMethods { @@ -49,4 +50,10 @@ object StaticMethods { throwable ) } + + fun getCurrentDateMinusSeconds(seconds: Int): Date { + val calendar = Calendar.getInstance() + calendar.add(Calendar.SECOND, -seconds) + return calendar.time + } } diff --git a/app/src/main/res/layout/fragment_display.xml b/app/src/main/res/layout/fragment_display.xml index 5961aa8..9a6f5d5 100644 --- a/app/src/main/res/layout/fragment_display.xml +++ b/app/src/main/res/layout/fragment_display.xml @@ -38,9 +38,7 @@ VTU CS Lab VTU CS Lab New Version - No Internet Connection. + No Internet Connection.\n\nThis content is not available offline yet, Kindly connect to the internet to sync this content locally. Some Error Occurred. Copy Git Repository diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 58b3344..1ca91d6 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -14,6 +14,16 @@ @color/colorAccent + +