From 6ab63a7ddb09f8aa2c85599a6bf6c5f7142d9299 Mon Sep 17 00:00:00 2001 From: Matte23 Date: Thu, 12 Nov 2020 10:25:02 +0100 Subject: [PATCH] Add shared module --- app/build.gradle.kts | 15 +-- .../circolapp/AlarmBroadcastReceiver.kt | 44 +++---- .../underdesk/circolapp/SettingsActivity.kt | 7 +- .../adapters/CircularLetterAdapter.kt | 55 ++++---- .../data/AndroidCircularRepository.kt | 20 +++ .../{Converters.kt => AndroidDatabase.kt} | 36 +++-- .../underdesk/circolapp/data/AppDatabase.kt | 49 ------- .../underdesk/circolapp/data/CircularDao.kt | 58 -------- .../circolapp/data/CircularRepository.kt | 49 ------- .../fragments/CircularLetterFragment.kt | 12 +- .../circolapp/fragments/FavouritesFragment.kt | 12 +- .../fragments/NewReminderFragment.kt | 59 ++++----- .../circolapp/fragments/RemindersFragment.kt | 12 +- .../intro/SchoolSelectionFragment.kt | 5 +- .../circolapp/server/AndroidServerApi.kt | 41 ++++++ .../underdesk/circolapp/server/ServerAPI.kt | 105 --------------- .../circolapp/server/curie/CurieServer.kt | 99 -------------- .../circolapp/server/curie/pojo/Content.kt | 8 -- .../circolapp/server/curie/pojo/Response.kt | 8 -- .../viewmodels/CircularLetterViewModel.kt | 8 +- .../CircularLetterViewModelFactory.kt | 2 +- .../viewmodels/FavouritesViewModel.kt | 13 +- .../viewmodels/FavouritesViewModelFactory.kt | 2 +- .../viewmodels/RemindersViewModel.kt | 13 +- .../viewmodels/RemindersViewModelFactory.kt | 2 +- .../net/underdesk/circolapp/works/PollWork.kt | 11 +- build.gradle.kts | 2 + buildSrc/src/main/kotlin/Config.kt | 6 +- buildSrc/src/main/kotlin/Dependencies.kt | 33 +++-- gradle.properties | 5 + settings.gradle.kts | 1 + shared/.gitignore | 1 + shared/build.gradle.kts | 124 ++++++++++++++++++ shared/src/androidMain/AndroidManifest.xml | 2 + .../circolapp/shared/PlatformDispatcher.kt | 7 + .../shared/data/DatabaseDriverFactory.kt | 11 ++ .../circolapp/shared/server/KtorFactory.kt | 18 +++ .../server/curie/SpecificCurieServer.kt | 24 ++++ .../porporato/SpecificPorporatoServer.kt | 27 ++++ .../circolapp/shared/PlatformDispatcher.kt | 7 + .../circolapp/shared}/data/Circular.kt | 10 +- .../circolapp/shared/data/CircularDao.kt | 87 ++++++++++++ .../shared/data/CircularRepository.kt | 36 +++++ .../shared/data/DatabaseDriverFactory.kt | 7 + .../circolapp/shared/server/KtorFactory.kt | 7 + .../circolapp/shared}/server/Server.kt | 4 +- .../circolapp/shared/server/ServerAPI.kt | 71 ++++++++++ .../shared/server/curie/CurieServer.kt | 73 +++++++++++ .../shared/server/pojo/WordpressResponse.kt | 13 ++ .../server/porporato/PorporatoServer.kt | 84 +++++------- .../circolapp/shared/utils/SqlUtils.kt | 29 ++++ .../AppDatabase.sq | 59 +++++++++ .../circolapp/shared/PlatformDispatcher.kt | 7 + .../shared/data/DatabaseDriverFactory.kt | 10 ++ .../circolapp/shared/server/KtorFactory.kt | 18 +++ .../server/curie/SpecificCurieServer.kt | 9 ++ .../porporato/SpecificPorporatoServer.kt | 9 ++ 57 files changed, 929 insertions(+), 617 deletions(-) create mode 100644 app/src/main/java/net/underdesk/circolapp/data/AndroidCircularRepository.kt rename app/src/main/java/net/underdesk/circolapp/data/{Converters.kt => AndroidDatabase.kt} (53%) delete mode 100644 app/src/main/java/net/underdesk/circolapp/data/AppDatabase.kt delete mode 100644 app/src/main/java/net/underdesk/circolapp/data/CircularDao.kt create mode 100644 app/src/main/java/net/underdesk/circolapp/server/AndroidServerApi.kt delete mode 100644 app/src/main/java/net/underdesk/circolapp/server/ServerAPI.kt delete mode 100644 app/src/main/java/net/underdesk/circolapp/server/curie/CurieServer.kt delete mode 100644 app/src/main/java/net/underdesk/circolapp/server/curie/pojo/Content.kt delete mode 100644 app/src/main/java/net/underdesk/circolapp/server/curie/pojo/Response.kt create mode 100644 shared/.gitignore create mode 100644 shared/build.gradle.kts create mode 100644 shared/src/androidMain/AndroidManifest.xml create mode 100644 shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/PlatformDispatcher.kt create mode 100644 shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/data/DatabaseDriverFactory.kt create mode 100644 shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/server/KtorFactory.kt create mode 100644 shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/server/curie/SpecificCurieServer.kt create mode 100644 shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/server/porporato/SpecificPorporatoServer.kt create mode 100644 shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/PlatformDispatcher.kt rename {app/src/main/java/net/underdesk/circolapp => shared/src/commonMain/kotlin/net/underdesk/circolapp/shared}/data/Circular.kt (82%) create mode 100644 shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularDao.kt create mode 100644 shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularRepository.kt create mode 100644 shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/DatabaseDriverFactory.kt create mode 100644 shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/KtorFactory.kt rename {app/src/main/java/net/underdesk/circolapp => shared/src/commonMain/kotlin/net/underdesk/circolapp/shared}/server/Server.kt (90%) create mode 100644 shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/ServerAPI.kt create mode 100644 shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/curie/CurieServer.kt create mode 100644 shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/pojo/WordpressResponse.kt rename {app/src/main/java/net/underdesk/circolapp => shared/src/commonMain/kotlin/net/underdesk/circolapp/shared}/server/porporato/PorporatoServer.kt (67%) create mode 100644 shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/utils/SqlUtils.kt create mode 100644 shared/src/commonMain/sqldelight/net.underdesk.circolapp.shared.data/AppDatabase.sq create mode 100644 shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/PlatformDispatcher.kt create mode 100644 shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/data/DatabaseDriverFactory.kt create mode 100644 shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/server/KtorFactory.kt create mode 100644 shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/server/curie/SpecificCurieServer.kt create mode 100644 shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/server/porporato/SpecificPorporatoServer.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9105be2..f310936 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,8 +46,13 @@ android { dependencies { implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) + // Shared module + implementation(project(":shared")) + // Kotlin implementation(Dependencies.Kotlin.core) + implementation(Dependencies.Kotlin.coroutinesCore) + implementation(Dependencies.Kotlin.coroutinesAndroid) // AndroidX implementation(Dependencies.AndroidX.appcompat) @@ -55,13 +60,11 @@ dependencies { implementation(Dependencies.AndroidX.constraintLayout) implementation(Dependencies.AndroidX.swipeRefreshLayout) implementation(Dependencies.AndroidX.lifecycleExtensions) + implementation(Dependencies.AndroidX.lifecycleLiveData) implementation(Dependencies.AndroidX.preference) implementation(Dependencies.AndroidX.navigationFragment) implementation(Dependencies.AndroidX.navigationUi) implementation(Dependencies.AndroidX.workManager) - implementation(Dependencies.AndroidX.Room.roomRuntime) - implementation(Dependencies.AndroidX.Room.roomKtx) - kapt(Dependencies.AndroidX.Room.roomCompiler) // Google implementation(Dependencies.Google.material) @@ -70,17 +73,11 @@ dependencies { implementation(platform(Dependencies.Firebase.bom)) implementation(Dependencies.Firebase.messaging) - // Square - implementation(Dependencies.Square.okhttp) - implementation(Dependencies.Square.moshi) - kapt(Dependencies.Square.moshiCodegen) - // AboutLibraries implementation(Dependencies.AboutLibraries.aboutLibrariesCore) implementation(Dependencies.AboutLibraries.aboutLibraries) // Misc - implementation(Dependencies.Misc.jsoup) implementation(Dependencies.Misc.appIntro) implementation(Dependencies.Misc.materialSpinner) diff --git a/app/src/main/java/net/underdesk/circolapp/AlarmBroadcastReceiver.kt b/app/src/main/java/net/underdesk/circolapp/AlarmBroadcastReceiver.kt index f5eb41f..4f860b2 100644 --- a/app/src/main/java/net/underdesk/circolapp/AlarmBroadcastReceiver.kt +++ b/app/src/main/java/net/underdesk/circolapp/AlarmBroadcastReceiver.kt @@ -29,8 +29,10 @@ import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.TaskStackBuilder -import net.underdesk.circolapp.data.AppDatabase -import net.underdesk.circolapp.data.Circular +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import net.underdesk.circolapp.data.AndroidDatabase +import net.underdesk.circolapp.shared.data.Circular class AlarmBroadcastReceiver : BroadcastReceiver() { @@ -41,27 +43,25 @@ class AlarmBroadcastReceiver : BroadcastReceiver() { } override fun onReceive(context: Context, intent: Intent) { - object : Thread() { - override fun run() { - createNotificationChannel(context) - val circular = AppDatabase.getInstance(context).circularDao().getCircular( - intent.getLongExtra( - CIRCULAR_ID, - 0 - ), - intent.getIntExtra( - SCHOOL_ID, - 0 - ) + GlobalScope.launch { + createNotificationChannel(context) + val circular = AndroidDatabase.getDaoInstance(context).getCircular( + intent.getLongExtra( + CIRCULAR_ID, + 0 + ), + intent.getIntExtra( + SCHOOL_ID, + 0 ) - createNotification( - context, - circular - ) - AppDatabase.getInstance(context).circularDao() - .update(circular.apply { reminder = false }) - } - }.start() + ) + createNotification( + context, + circular + ) + AndroidDatabase.getDaoInstance(context) + .update(circular.id, circular.school, circular.favourite, false) + } } private fun createNotification(context: Context, circular: Circular) { diff --git a/app/src/main/java/net/underdesk/circolapp/SettingsActivity.kt b/app/src/main/java/net/underdesk/circolapp/SettingsActivity.kt index 3de6963..fe33532 100644 --- a/app/src/main/java/net/underdesk/circolapp/SettingsActivity.kt +++ b/app/src/main/java/net/underdesk/circolapp/SettingsActivity.kt @@ -26,7 +26,8 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.preference.* import kotlinx.android.synthetic.main.settings_activity.* import net.underdesk.circolapp.push.FirebaseTopicUtils -import net.underdesk.circolapp.server.ServerAPI +import net.underdesk.circolapp.server.AndroidServerApi +import net.underdesk.circolapp.shared.server.ServerAPI import net.underdesk.circolapp.works.PollWork class SettingsActivity : AppCompatActivity() { @@ -55,7 +56,7 @@ class SettingsActivity : AppCompatActivity() { schoolPreference?.let { setSchoolListPreference(it) } val schoolPreferenceListener = Preference.OnPreferenceChangeListener { _, value -> - ServerAPI.changeServer(value.toString().toInt(), requireContext()) + AndroidServerApi.changeServer(value.toString().toInt(), requireContext()) true } schoolPreference?.onPreferenceChangeListener = schoolPreferenceListener @@ -109,7 +110,7 @@ class SettingsActivity : AppCompatActivity() { val enablePolling = sharedPreferences.getBoolean("enable_polling", false) if (notifyNewCirculars && !enablePolling) { - val serverID = ServerAPI.getInstance(requireContext()).serverID() + val serverID = AndroidServerApi.getInstance(requireContext()).serverID() val serverToken = ServerAPI.Companion.Servers.values()[serverID].toString() FirebaseTopicUtils.selectTopic(serverToken, requireContext()) diff --git a/app/src/main/java/net/underdesk/circolapp/adapters/CircularLetterAdapter.kt b/app/src/main/java/net/underdesk/circolapp/adapters/CircularLetterAdapter.kt index 80fe51f..9982ff7 100644 --- a/app/src/main/java/net/underdesk/circolapp/adapters/CircularLetterAdapter.kt +++ b/app/src/main/java/net/underdesk/circolapp/adapters/CircularLetterAdapter.kt @@ -32,17 +32,20 @@ import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.item_circular.view.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import net.underdesk.circolapp.AlarmBroadcastReceiver import net.underdesk.circolapp.R -import net.underdesk.circolapp.data.AppDatabase -import net.underdesk.circolapp.data.Circular +import net.underdesk.circolapp.data.AndroidDatabase import net.underdesk.circolapp.fragments.NewReminderFragment +import net.underdesk.circolapp.shared.data.Circular import net.underdesk.circolapp.utils.DownloadableFile import net.underdesk.circolapp.utils.FileUtils class CircularLetterAdapter( private var circulars: List, - private val adapterCallback: AdapterCallback + private val adapterCallback: AdapterCallback, + private val adapterScope: CoroutineScope ) : RecyclerView.Adapter() { private lateinit var context: Context @@ -158,31 +161,37 @@ class CircularLetterAdapter( } holder.favouriteButton.setOnClickListener { - object : Thread() { - override fun run() { - AppDatabase.getInstance(context).circularDao() - .update(circulars[position].apply { favourite = !favourite }) - } - }.start() + adapterScope.launch { + AndroidDatabase.getDaoInstance(context).update( + circulars[position].id, + circulars[position].school, + !circulars[position].favourite, + circulars[position].reminder + ) + } } holder.reminderButton.setOnClickListener { if (circulars[position].reminder) { - object : Thread() { - override fun run() { - val pendingIntent = PendingIntent.getBroadcast( - context, - circulars[position].id.toInt(), - Intent(context, AlarmBroadcastReceiver::class.java), - 0 + + adapterScope.launch { + val pendingIntent = PendingIntent.getBroadcast( + context, + circulars[position].id.toInt(), + Intent(context, AlarmBroadcastReceiver::class.java), + 0 + ) + + pendingIntent.cancel() + + AndroidDatabase.getDaoInstance(context) + .update( + circulars[position].id, + circulars[position].school, + circulars[position].favourite, + false ) - - pendingIntent.cancel() - - AppDatabase.getInstance(context).circularDao() - .update(circulars[position].apply { reminder = false }) - } - }.start() + } } else { NewReminderFragment.create(circulars[position]) .show((context as FragmentActivity).supportFragmentManager, "NewReminderDialog") diff --git a/app/src/main/java/net/underdesk/circolapp/data/AndroidCircularRepository.kt b/app/src/main/java/net/underdesk/circolapp/data/AndroidCircularRepository.kt new file mode 100644 index 0000000..10ac83c --- /dev/null +++ b/app/src/main/java/net/underdesk/circolapp/data/AndroidCircularRepository.kt @@ -0,0 +1,20 @@ +package net.underdesk.circolapp.data + +import android.content.Context +import net.underdesk.circolapp.server.AndroidServerApi +import net.underdesk.circolapp.shared.data.CircularDao +import net.underdesk.circolapp.shared.data.CircularRepository +import net.underdesk.circolapp.shared.server.ServerAPI + +object AndroidCircularRepository { + @Volatile + private var instance: CircularRepository? = null + + fun getInstance(circularDao: CircularDao, serverAPI: ServerAPI) = + instance ?: synchronized(this) { + instance ?: CircularRepository(circularDao, serverAPI).also { instance = it } + } + + fun getInstance(context: Context) = + getInstance(AndroidDatabase.getDaoInstance(context), AndroidServerApi.getInstance(context)) +} diff --git a/app/src/main/java/net/underdesk/circolapp/data/Converters.kt b/app/src/main/java/net/underdesk/circolapp/data/AndroidDatabase.kt similarity index 53% rename from app/src/main/java/net/underdesk/circolapp/data/Converters.kt rename to app/src/main/java/net/underdesk/circolapp/data/AndroidDatabase.kt index a7db408..70135c6 100644 --- a/app/src/main/java/net/underdesk/circolapp/data/Converters.kt +++ b/app/src/main/java/net/underdesk/circolapp/data/AndroidDatabase.kt @@ -18,31 +18,27 @@ package net.underdesk.circolapp.data -import androidx.room.TypeConverter +import android.content.Context +import net.underdesk.circolapp.shared.data.AppDatabase +import net.underdesk.circolapp.shared.data.CircularDao +import net.underdesk.circolapp.shared.data.DatabaseDriverFactory -class Converters { +object AndroidDatabase { - @TypeConverter - fun stringToList(data: String?): List { - val list: MutableList = mutableListOf() + @Volatile + private var instance: AppDatabase? = null - if (data != null) { - for (attachment in data.split("˜")) { - list.add(attachment) - } + fun getInstance(context: Context): AppDatabase { + return instance ?: synchronized(this) { + instance ?: AppDatabase( + DatabaseDriverFactory( + context + ).createDriver() + ).also { instance = it } } - - return list.dropLast(1) } - @TypeConverter - fun listToString(list: List): String { - var string = "" - - for (attachment in list) { - string += "$attachment˜" - } - - return string + fun getDaoInstance(context: Context): CircularDao { + return CircularDao(getInstance(context)) } } diff --git a/app/src/main/java/net/underdesk/circolapp/data/AppDatabase.kt b/app/src/main/java/net/underdesk/circolapp/data/AppDatabase.kt deleted file mode 100644 index 23b5f06..0000000 --- a/app/src/main/java/net/underdesk/circolapp/data/AppDatabase.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Circolapp - * Copyright (C) 2019-2020 Matteo Schiff - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.underdesk.circolapp.data - -import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters - -@Database(entities = [Circular::class], version = 2, exportSchema = false) -@TypeConverters(Converters::class) -abstract class AppDatabase : RoomDatabase() { - abstract fun circularDao(): CircularDao - - companion object { - - @Volatile - private var instance: AppDatabase? = null - - private const val DATABASE_NAME = "database" - - fun getInstance(context: Context): AppDatabase { - return instance ?: synchronized(this) { - instance ?: Room.databaseBuilder( - context, - AppDatabase::class.java, - DATABASE_NAME - ).fallbackToDestructiveMigration().build().also { instance = it } - } - } - } -} diff --git a/app/src/main/java/net/underdesk/circolapp/data/CircularDao.kt b/app/src/main/java/net/underdesk/circolapp/data/CircularDao.kt deleted file mode 100644 index e5a79fb..0000000 --- a/app/src/main/java/net/underdesk/circolapp/data/CircularDao.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Circolapp - * Copyright (C) 2019-2020 Matteo Schiff - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.underdesk.circolapp.data - -import androidx.lifecycle.LiveData -import androidx.room.* - -@Dao -interface CircularDao { - @Query("SELECT * FROM circulars WHERE school is :school ORDER BY id DESC") - fun getCirculars(school: Int): List - - @Query("SELECT * FROM circulars WHERE school is :school ORDER BY id DESC") - fun getLiveCirculars(school: Int): LiveData> - - @Query("SELECT * FROM circulars WHERE school is :school AND name LIKE :query ORDER BY id DESC") - fun searchCirculars(query: String, school: Int): LiveData> - - @Query("SELECT * FROM circulars WHERE school is :school AND id = :id ORDER BY id DESC") - fun getCircular(id: Long, school: Int): Circular - - @Query("SELECT * FROM circulars WHERE school is :school AND favourite ORDER BY id DESC") - fun getFavourites(school: Int): LiveData> - - @Query("SELECT * FROM circulars WHERE school is :school AND favourite AND name LIKE :query ORDER BY id DESC") - fun searchFavourites(query: String, school: Int): LiveData> - - @Query("SELECT * FROM circulars WHERE school is :school AND reminder ORDER BY id DESC") - fun getReminders(school: Int): LiveData> - - @Query("SELECT * FROM circulars WHERE school is :school AND reminder AND name LIKE :query ORDER BY id DESC") - fun searchReminders(query: String, school: Int): LiveData> - - @Insert(onConflict = OnConflictStrategy.IGNORE) - fun insertAll(circulars: List) - - @Update - fun update(circular: Circular) - - @Query("DELETE FROM circulars") - fun deleteAll() -} diff --git a/app/src/main/java/net/underdesk/circolapp/data/CircularRepository.kt b/app/src/main/java/net/underdesk/circolapp/data/CircularRepository.kt index 5a04edc..f3038bd 100644 --- a/app/src/main/java/net/underdesk/circolapp/data/CircularRepository.kt +++ b/app/src/main/java/net/underdesk/circolapp/data/CircularRepository.kt @@ -1,50 +1 @@ package net.underdesk.circolapp.data - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import net.underdesk.circolapp.server.ServerAPI - -class CircularRepository( - val circularDao: CircularDao, - private val serverAPI: ServerAPI -) { - suspend fun updateCirculars(returnNewCirculars: Boolean = true): Pair, Boolean> { - var onlyNewCirculars = listOf() - - return withContext(Dispatchers.IO) { - val result = serverAPI.getCircularsFromServer() - if (result.second == ServerAPI.Companion.Result.ERROR) - return@withContext Pair(emptyList(), false) - - val oldCirculars = circularDao.getCirculars(serverAPI.serverID()) - val newCirculars = result.first - - if (newCirculars.size != oldCirculars.size) { - if (newCirculars.size < oldCirculars.size) { - circularDao.deleteAll() - } - - if (returnNewCirculars) { - val oldCircularsSize = - if (newCirculars.size < oldCirculars.size) 0 else oldCirculars.size - - val circularCount = newCirculars.size - oldCircularsSize - onlyNewCirculars = newCirculars.subList(0, circularCount) - } - - circularDao.insertAll(newCirculars) - } - Pair(onlyNewCirculars, true) - } - } - - companion object { - @Volatile - private var instance: CircularRepository? = null - - fun getInstance(circularDao: CircularDao, serverAPI: ServerAPI) = - instance ?: synchronized(this) { - instance ?: CircularRepository(circularDao, serverAPI).also { instance = it } - } - } -} diff --git a/app/src/main/java/net/underdesk/circolapp/fragments/CircularLetterFragment.kt b/app/src/main/java/net/underdesk/circolapp/fragments/CircularLetterFragment.kt index cf9a529..194dea1 100644 --- a/app/src/main/java/net/underdesk/circolapp/fragments/CircularLetterFragment.kt +++ b/app/src/main/java/net/underdesk/circolapp/fragments/CircularLetterFragment.kt @@ -25,6 +25,7 @@ import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.fragment_circular_letters.* @@ -32,9 +33,7 @@ import kotlinx.android.synthetic.main.fragment_circular_letters.view.* import net.underdesk.circolapp.MainActivity import net.underdesk.circolapp.R import net.underdesk.circolapp.adapters.CircularLetterAdapter -import net.underdesk.circolapp.data.AppDatabase -import net.underdesk.circolapp.data.CircularRepository -import net.underdesk.circolapp.server.ServerAPI +import net.underdesk.circolapp.data.AndroidCircularRepository import net.underdesk.circolapp.viewmodels.CircularLetterViewModel import net.underdesk.circolapp.viewmodels.CircularLetterViewModelFactory @@ -45,10 +44,7 @@ class CircularLetterFragment : private val circularLetterViewModel: CircularLetterViewModel by viewModels { CircularLetterViewModelFactory( - CircularRepository.getInstance( - AppDatabase.getInstance(requireContext()).circularDao(), - ServerAPI.getInstance(requireContext()) - ), + AndroidCircularRepository.getInstance(requireContext()), requireActivity().application ) } @@ -67,7 +63,7 @@ class CircularLetterFragment : { if (root.circulars_list.adapter == null) { root.circulars_list.adapter = - CircularLetterAdapter(it, activity as MainActivity) + CircularLetterAdapter(it, activity as MainActivity, lifecycleScope) } else { (root.circulars_list.adapter as CircularLetterAdapter).changeDataSet(it) } diff --git a/app/src/main/java/net/underdesk/circolapp/fragments/FavouritesFragment.kt b/app/src/main/java/net/underdesk/circolapp/fragments/FavouritesFragment.kt index 5108909..e900413 100644 --- a/app/src/main/java/net/underdesk/circolapp/fragments/FavouritesFragment.kt +++ b/app/src/main/java/net/underdesk/circolapp/fragments/FavouritesFragment.kt @@ -24,14 +24,13 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.fragment_circular_letters.view.* import net.underdesk.circolapp.MainActivity import net.underdesk.circolapp.R import net.underdesk.circolapp.adapters.CircularLetterAdapter -import net.underdesk.circolapp.data.AppDatabase -import net.underdesk.circolapp.data.CircularRepository -import net.underdesk.circolapp.server.ServerAPI +import net.underdesk.circolapp.data.AndroidCircularRepository import net.underdesk.circolapp.viewmodels.FavouritesViewModel import net.underdesk.circolapp.viewmodels.FavouritesViewModelFactory @@ -39,10 +38,7 @@ class FavouritesFragment : Fragment(), MainActivity.SearchCallback { private val favouritesViewModel: FavouritesViewModel by viewModels { FavouritesViewModelFactory( - CircularRepository.getInstance( - AppDatabase.getInstance(requireContext()).circularDao(), - ServerAPI.getInstance(requireContext()) - ), + AndroidCircularRepository.getInstance(requireContext()), requireActivity().application ) } @@ -62,7 +58,7 @@ class FavouritesFragment : Fragment(), MainActivity.SearchCallback { { if (root.circulars_list.adapter == null) { root.circulars_list.adapter = - CircularLetterAdapter(it, activity as MainActivity) + CircularLetterAdapter(it, activity as MainActivity, lifecycleScope) } else { (root.circulars_list.adapter as CircularLetterAdapter).changeDataSet(it) } diff --git a/app/src/main/java/net/underdesk/circolapp/fragments/NewReminderFragment.kt b/app/src/main/java/net/underdesk/circolapp/fragments/NewReminderFragment.kt index 5bd4e1e..1f73588 100644 --- a/app/src/main/java/net/underdesk/circolapp/fragments/NewReminderFragment.kt +++ b/app/src/main/java/net/underdesk/circolapp/fragments/NewReminderFragment.kt @@ -29,36 +29,33 @@ import android.view.View import android.view.ViewGroup import androidx.core.app.AlarmManagerCompat import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope import kotlinx.android.synthetic.main.dialog_reminder.* +import kotlinx.coroutines.launch import net.underdesk.circolapp.AlarmBroadcastReceiver import net.underdesk.circolapp.R -import net.underdesk.circolapp.data.AppDatabase -import net.underdesk.circolapp.data.Circular +import net.underdesk.circolapp.data.AndroidDatabase +import net.underdesk.circolapp.shared.data.Circular import java.util.* class NewReminderFragment : DialogFragment() { companion object { - private const val CIRCULAR = "circular" - fun create(circular: Circular): NewReminderFragment { val dialog = NewReminderFragment() - dialog.arguments = Bundle().apply { - putParcelable(CIRCULAR, circular) - } + dialog.circular = circular return dialog } } private var dateNotChosen = true - var circular: Circular? = null + lateinit var circular: Circular override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - circular = arguments?.getParcelable(CIRCULAR) return inflater.inflate(R.layout.dialog_reminder, container) } @@ -101,33 +98,31 @@ class NewReminderFragment : DialogFragment() { minute ) - object : Thread() { - override fun run() { - context?.let { context -> - circular?.let { circular -> - AppDatabase.getInstance(context).circularDao() - .update(circular.apply { reminder = true }) + lifecycleScope.launch { + context?.let { context -> + circular.let { circular -> + AndroidDatabase.getDaoInstance(context) + .update(circular.id, circular.school, circular.favourite, true) - val pendingIntent = PendingIntent.getBroadcast( - context, - circular.id.toInt(), - Intent(context, AlarmBroadcastReceiver::class.java) - .putExtra(AlarmBroadcastReceiver.CIRCULAR_ID, circular.id) - .putExtra(AlarmBroadcastReceiver.SCHOOL_ID, circular.school), - 0 - ) + val pendingIntent = PendingIntent.getBroadcast( + context, + circular.id.toInt(), + Intent(context, AlarmBroadcastReceiver::class.java) + .putExtra(AlarmBroadcastReceiver.CIRCULAR_ID, circular.id) + .putExtra(AlarmBroadcastReceiver.SCHOOL_ID, circular.school), + 0 + ) - AlarmManagerCompat.setExactAndAllowWhileIdle( - context.getSystemService(Context.ALARM_SERVICE) as AlarmManager, - AlarmManager.RTC_WAKEUP, - calendar.timeInMillis, - pendingIntent - ) - } + AlarmManagerCompat.setExactAndAllowWhileIdle( + context.getSystemService(Context.ALARM_SERVICE) as AlarmManager, + AlarmManager.RTC_WAKEUP, + calendar.timeInMillis, + pendingIntent + ) } - dismiss() } - }.start() + dismiss() + } } } diff --git a/app/src/main/java/net/underdesk/circolapp/fragments/RemindersFragment.kt b/app/src/main/java/net/underdesk/circolapp/fragments/RemindersFragment.kt index 43bafe8..99ba621 100644 --- a/app/src/main/java/net/underdesk/circolapp/fragments/RemindersFragment.kt +++ b/app/src/main/java/net/underdesk/circolapp/fragments/RemindersFragment.kt @@ -24,14 +24,13 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import kotlinx.android.synthetic.main.fragment_circular_letters.view.* import net.underdesk.circolapp.MainActivity import net.underdesk.circolapp.R import net.underdesk.circolapp.adapters.CircularLetterAdapter -import net.underdesk.circolapp.data.AppDatabase -import net.underdesk.circolapp.data.CircularRepository -import net.underdesk.circolapp.server.ServerAPI +import net.underdesk.circolapp.data.AndroidCircularRepository import net.underdesk.circolapp.viewmodels.RemindersViewModel import net.underdesk.circolapp.viewmodels.RemindersViewModelFactory @@ -39,10 +38,7 @@ class RemindersFragment : Fragment(), MainActivity.SearchCallback { private val remindersViewModel: RemindersViewModel by viewModels { RemindersViewModelFactory( - CircularRepository.getInstance( - AppDatabase.getInstance(requireContext()).circularDao(), - ServerAPI.getInstance(requireContext()) - ), + AndroidCircularRepository.getInstance(requireContext()), requireActivity().application ) } @@ -62,7 +58,7 @@ class RemindersFragment : Fragment(), MainActivity.SearchCallback { { if (root.circulars_list.adapter == null) { root.circulars_list.adapter = - CircularLetterAdapter(it, activity as MainActivity) + CircularLetterAdapter(it, activity as MainActivity, lifecycleScope) } else { (root.circulars_list.adapter as CircularLetterAdapter).changeDataSet(it) } diff --git a/app/src/main/java/net/underdesk/circolapp/fragments/intro/SchoolSelectionFragment.kt b/app/src/main/java/net/underdesk/circolapp/fragments/intro/SchoolSelectionFragment.kt index 629ab88..fea6459 100644 --- a/app/src/main/java/net/underdesk/circolapp/fragments/intro/SchoolSelectionFragment.kt +++ b/app/src/main/java/net/underdesk/circolapp/fragments/intro/SchoolSelectionFragment.kt @@ -12,7 +12,8 @@ import com.github.appintro.SlidePolicy import com.tiper.MaterialSpinner import kotlinx.android.synthetic.main.fragment_school_selection.view.* import net.underdesk.circolapp.R -import net.underdesk.circolapp.server.ServerAPI +import net.underdesk.circolapp.server.AndroidServerApi +import net.underdesk.circolapp.shared.server.ServerAPI class SchoolSelectionFragment : Fragment(), SlidePolicy, MaterialSpinner.OnItemSelectedListener { private lateinit var preferenceManager: SharedPreferences @@ -53,7 +54,7 @@ class SchoolSelectionFragment : Fragment(), SlidePolicy, MaterialSpinner.OnItemS editor.putString("school", position.toString()) editor.apply() - ServerAPI.changeServer(position, requireContext()) + AndroidServerApi.changeServer(position, requireContext()) schoolSelected = true parent.error = null diff --git a/app/src/main/java/net/underdesk/circolapp/server/AndroidServerApi.kt b/app/src/main/java/net/underdesk/circolapp/server/AndroidServerApi.kt new file mode 100644 index 0000000..12b16e4 --- /dev/null +++ b/app/src/main/java/net/underdesk/circolapp/server/AndroidServerApi.kt @@ -0,0 +1,41 @@ +package net.underdesk.circolapp.server + +import android.content.Context +import androidx.preference.PreferenceManager +import net.underdesk.circolapp.push.FirebaseTopicUtils +import net.underdesk.circolapp.shared.server.ServerAPI + +object AndroidServerApi { + @Volatile + private var instance: ServerAPI? = null + + fun getInstance(server: ServerAPI.Companion.Servers): ServerAPI { + return instance ?: synchronized(this) { + instance ?: ServerAPI(ServerAPI.createServer(server)).also { instance = it } + } + } + + fun getInstance(context: Context): ServerAPI { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + val serverID = sharedPreferences.getString("school", "0")?.toInt() ?: 0 + + val server = ServerAPI.Companion.Servers.values()[serverID] + + return instance ?: synchronized(this) { + instance ?: ServerAPI(ServerAPI.createServer(server)).also { instance = it } + } + } + + fun changeServer(index: Int, context: Context) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + val newServer = ServerAPI.Companion.Servers.values()[index] + + val notifyNewCirculars = sharedPreferences.getBoolean("notify_new_circulars", true) + val enablePolling = sharedPreferences.getBoolean("enable_polling", false) + + if (notifyNewCirculars && !enablePolling) + FirebaseTopicUtils.selectTopic(newServer.toString(), context) + + instance?.changeServer(ServerAPI.createServer(newServer)) + } +} diff --git a/app/src/main/java/net/underdesk/circolapp/server/ServerAPI.kt b/app/src/main/java/net/underdesk/circolapp/server/ServerAPI.kt deleted file mode 100644 index cdce8a2..0000000 --- a/app/src/main/java/net/underdesk/circolapp/server/ServerAPI.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Circolapp - * Copyright (C) 2019-2020 Matteo Schiff - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package net.underdesk.circolapp.server - -import android.content.Context -import androidx.preference.PreferenceManager -import net.underdesk.circolapp.data.Circular -import net.underdesk.circolapp.push.FirebaseTopicUtils -import net.underdesk.circolapp.server.curie.CurieServer -import net.underdesk.circolapp.server.porporato.PorporatoServer - -class ServerAPI( - private var server: Server -) { - fun serverID(): Int = server.serverID - - suspend fun getCircularsFromServer(): Pair, Result> { - val newCircularsAvailable = server.newCircularsAvailable() - - if (newCircularsAvailable.second == Result.ERROR) - return Pair(emptyList(), Result.ERROR) - - if (!newCircularsAvailable.first) - return Pair(emptyList(), Result.SUCCESS) - - return server.getCircularsFromServer() - } - - fun changeServer(server: Server) { - this.server = server - } - - companion object { - enum class Servers { - CURIE, PORPORATO - } - - enum class Result { - SUCCESS, ERROR - } - - fun getServerId(server: Servers): Int { - return Servers.values().indexOf(server) - } - - fun getServerName(server: Servers) = when (server) { - Servers.CURIE -> "Liceo scientifico Maria Curie" - Servers.PORPORATO -> "Liceo G.F. Porporato" - } - - @Volatile - private var instance: ServerAPI? = null - - fun getInstance(server: Servers): ServerAPI { - return instance ?: synchronized(this) { - instance ?: ServerAPI(createServer(server)).also { instance = it } - } - } - - fun getInstance(context: Context): ServerAPI { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - val serverID = sharedPreferences.getString("school", "0")?.toInt() ?: 0 - - val server = Servers.values()[serverID] - - return instance ?: synchronized(this) { - instance ?: ServerAPI(createServer(server)).also { instance = it } - } - } - - fun changeServer(index: Int, context: Context) { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - val newServer = Servers.values()[index] - - val notifyNewCirculars = sharedPreferences.getBoolean("notify_new_circulars", true) - val enablePolling = sharedPreferences.getBoolean("enable_polling", false) - - if (notifyNewCirculars && !enablePolling) - FirebaseTopicUtils.selectTopic(newServer.toString(), context) - - instance?.changeServer(createServer(newServer)) - } - - private fun createServer(server: Servers) = when (server) { - Servers.CURIE -> CurieServer() - Servers.PORPORATO -> PorporatoServer() - } - } -} diff --git a/app/src/main/java/net/underdesk/circolapp/server/curie/CurieServer.kt b/app/src/main/java/net/underdesk/circolapp/server/curie/CurieServer.kt deleted file mode 100644 index 8e80d18..0000000 --- a/app/src/main/java/net/underdesk/circolapp/server/curie/CurieServer.kt +++ /dev/null @@ -1,99 +0,0 @@ -package net.underdesk.circolapp.server.curie - -import com.squareup.moshi.Moshi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import net.underdesk.circolapp.data.Circular -import net.underdesk.circolapp.server.Server -import net.underdesk.circolapp.server.ServerAPI -import net.underdesk.circolapp.server.curie.pojo.Response -import okhttp3.OkHttpClient -import okhttp3.Request -import org.jsoup.Jsoup -import java.io.IOException -import java.util.regex.Pattern - -class CurieServer : Server() { - private val moshi = Moshi.Builder().build() - private val responseAdapter = moshi.adapter(Response::class.java) - private val client = OkHttpClient() - - override val serverID = ServerAPI.getServerId(ServerAPI.Companion.Servers.CURIE) - - override suspend fun getCircularsFromServer(): Pair, ServerAPI.Companion.Result> { - return try { - withContext(Dispatchers.Default) { - val json = retrieveDataFromServer() - - val document = Jsoup.parseBodyFragment(json.content.rendered) - val htmlList = document.getElementsByTag("ul")[0].getElementsByTag("a") - - val list = ArrayList() - - htmlList.forEach { element -> - if (element.parents().size == 6) { - list.last().attachmentsNames.add(element.text()) - list.last().attachmentsUrls.add(element.attr("href")) - } else if (element.parents().size == 4) { - list.add(generateFromString(element.text(), element.attr("href"))) - } - } - Pair(list, ServerAPI.Companion.Result.SUCCESS) - } - } catch (exception: IOException) { - Pair(emptyList(), ServerAPI.Companion.Result.ERROR) - } - } - - override suspend fun newCircularsAvailable(): Pair { - return Pair(true, ServerAPI.Companion.Result.SUCCESS) - } - - @Throws(IOException::class) - private suspend fun retrieveDataFromServer(): Response { - val request = Request.Builder() - .url(ENDPOINT_URL) - .build() - - return withContext(Dispatchers.IO) { - val response = client.newCall(request).execute() - - if (!response.isSuccessful) { - throw IOException("HTTP error code: ${response.code})") - } - - responseAdapter.fromJson( - response.body!!.string() - )!! - } - } - - private fun generateFromString(string: String, url: String): Circular { - val idRegex = - """(\d+)""" - val matcherId = Pattern.compile(idRegex).matcher(string) - matcherId.find() - val id = matcherId.group(1) - - val dateRegex = - """(\d{2}/\d{2}/\d{4})""" - val matcherDate = Pattern.compile(dateRegex).matcher(string) - - var title = string.removeSuffix("-signed") - - return if (matcherDate.find()) { - title = title.removeRange(0, matcherDate.end()) - .removePrefix(" ") - .removePrefix("_") - .removePrefix(" ") - - Circular(id.toLong(), serverID, title, url, matcherDate.group(1) ?: "") - } else { - Circular(id.toLong(), serverID, title, url, "") - } - } - - companion object { - const val ENDPOINT_URL = "https://www.curiepinerolo.edu.it/wp-json/wp/v2/pages/5958" - } -} diff --git a/app/src/main/java/net/underdesk/circolapp/server/curie/pojo/Content.kt b/app/src/main/java/net/underdesk/circolapp/server/curie/pojo/Content.kt deleted file mode 100644 index 79fe93f..0000000 --- a/app/src/main/java/net/underdesk/circolapp/server/curie/pojo/Content.kt +++ /dev/null @@ -1,8 +0,0 @@ -package net.underdesk.circolapp.server.curie.pojo - -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class Content( - val rendered: String -) diff --git a/app/src/main/java/net/underdesk/circolapp/server/curie/pojo/Response.kt b/app/src/main/java/net/underdesk/circolapp/server/curie/pojo/Response.kt deleted file mode 100644 index 08ce81b..0000000 --- a/app/src/main/java/net/underdesk/circolapp/server/curie/pojo/Response.kt +++ /dev/null @@ -1,8 +0,0 @@ -package net.underdesk.circolapp.server.curie.pojo - -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class Response( - val content: Content -) diff --git a/app/src/main/java/net/underdesk/circolapp/viewmodels/CircularLetterViewModel.kt b/app/src/main/java/net/underdesk/circolapp/viewmodels/CircularLetterViewModel.kt index 946144a..cde31c8 100644 --- a/app/src/main/java/net/underdesk/circolapp/viewmodels/CircularLetterViewModel.kt +++ b/app/src/main/java/net/underdesk/circolapp/viewmodels/CircularLetterViewModel.kt @@ -23,8 +23,8 @@ import android.content.SharedPreferences import androidx.lifecycle.* import androidx.preference.PreferenceManager import kotlinx.coroutines.launch -import net.underdesk.circolapp.data.Circular -import net.underdesk.circolapp.data.CircularRepository +import net.underdesk.circolapp.shared.data.Circular +import net.underdesk.circolapp.shared.data.CircularRepository import net.underdesk.circolapp.utils.DoubleTrigger class CircularLetterViewModel internal constructor( @@ -40,12 +40,12 @@ class CircularLetterViewModel internal constructor( val circulars: LiveData> = Transformations.switchMap(DoubleTrigger(query, schoolID)) { input -> if (input.first == null || input.first == "") { - circularRepository.circularDao.getLiveCirculars(input.second ?: 0) + circularRepository.circularDao.getFlowCirculars(input.second ?: 0).asLiveData() } else { circularRepository.circularDao.searchCirculars( "%${input.first}%", input.second ?: 0 - ) + ).asLiveData() } } diff --git a/app/src/main/java/net/underdesk/circolapp/viewmodels/CircularLetterViewModelFactory.kt b/app/src/main/java/net/underdesk/circolapp/viewmodels/CircularLetterViewModelFactory.kt index 5e29acd..24797ed 100644 --- a/app/src/main/java/net/underdesk/circolapp/viewmodels/CircularLetterViewModelFactory.kt +++ b/app/src/main/java/net/underdesk/circolapp/viewmodels/CircularLetterViewModelFactory.kt @@ -3,7 +3,7 @@ package net.underdesk.circolapp.viewmodels import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import net.underdesk.circolapp.data.CircularRepository +import net.underdesk.circolapp.shared.data.CircularRepository class CircularLetterViewModelFactory( private val circularRepository: CircularRepository, diff --git a/app/src/main/java/net/underdesk/circolapp/viewmodels/FavouritesViewModel.kt b/app/src/main/java/net/underdesk/circolapp/viewmodels/FavouritesViewModel.kt index c412a3b..a3957c4 100644 --- a/app/src/main/java/net/underdesk/circolapp/viewmodels/FavouritesViewModel.kt +++ b/app/src/main/java/net/underdesk/circolapp/viewmodels/FavouritesViewModel.kt @@ -20,13 +20,10 @@ package net.underdesk.circolapp.viewmodels import android.app.Application import android.content.SharedPreferences -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations +import androidx.lifecycle.* import androidx.preference.PreferenceManager -import net.underdesk.circolapp.data.Circular -import net.underdesk.circolapp.data.CircularRepository +import net.underdesk.circolapp.shared.data.Circular +import net.underdesk.circolapp.shared.data.CircularRepository import net.underdesk.circolapp.utils.DoubleTrigger class FavouritesViewModel internal constructor( @@ -46,12 +43,12 @@ class FavouritesViewModel internal constructor( val circulars: LiveData> = Transformations.switchMap(DoubleTrigger(query, schoolID)) { input -> if (input.first == null || input.first == "") { - circularRepository.circularDao.getFavourites(input.second ?: 0) + circularRepository.circularDao.getFavourites(input.second ?: 0).asLiveData() } else { circularRepository.circularDao.searchFavourites( "%${input.first}%", input.second ?: 0 - ) + ).asLiveData() } } diff --git a/app/src/main/java/net/underdesk/circolapp/viewmodels/FavouritesViewModelFactory.kt b/app/src/main/java/net/underdesk/circolapp/viewmodels/FavouritesViewModelFactory.kt index 4636eb9..75a197e 100644 --- a/app/src/main/java/net/underdesk/circolapp/viewmodels/FavouritesViewModelFactory.kt +++ b/app/src/main/java/net/underdesk/circolapp/viewmodels/FavouritesViewModelFactory.kt @@ -3,7 +3,7 @@ package net.underdesk.circolapp.viewmodels import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import net.underdesk.circolapp.data.CircularRepository +import net.underdesk.circolapp.shared.data.CircularRepository class FavouritesViewModelFactory( private val circularRepository: CircularRepository, diff --git a/app/src/main/java/net/underdesk/circolapp/viewmodels/RemindersViewModel.kt b/app/src/main/java/net/underdesk/circolapp/viewmodels/RemindersViewModel.kt index 092cf36..fcdade2 100644 --- a/app/src/main/java/net/underdesk/circolapp/viewmodels/RemindersViewModel.kt +++ b/app/src/main/java/net/underdesk/circolapp/viewmodels/RemindersViewModel.kt @@ -20,13 +20,10 @@ package net.underdesk.circolapp.viewmodels import android.app.Application import android.content.SharedPreferences -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations +import androidx.lifecycle.* import androidx.preference.PreferenceManager -import net.underdesk.circolapp.data.Circular -import net.underdesk.circolapp.data.CircularRepository +import net.underdesk.circolapp.shared.data.Circular +import net.underdesk.circolapp.shared.data.CircularRepository import net.underdesk.circolapp.utils.DoubleTrigger class RemindersViewModel internal constructor( @@ -46,12 +43,12 @@ class RemindersViewModel internal constructor( val circulars: LiveData> = Transformations.switchMap(DoubleTrigger(query, schoolID)) { input -> if (input.first == null || input.first == "") { - circularRepository.circularDao.getReminders(input.second ?: 0) + circularRepository.circularDao.getReminders(input.second ?: 0).asLiveData() } else { circularRepository.circularDao.searchReminders( "%${input.first}%", input.second ?: 0 - ) + ).asLiveData() } } diff --git a/app/src/main/java/net/underdesk/circolapp/viewmodels/RemindersViewModelFactory.kt b/app/src/main/java/net/underdesk/circolapp/viewmodels/RemindersViewModelFactory.kt index af831ce..128ae23 100644 --- a/app/src/main/java/net/underdesk/circolapp/viewmodels/RemindersViewModelFactory.kt +++ b/app/src/main/java/net/underdesk/circolapp/viewmodels/RemindersViewModelFactory.kt @@ -3,7 +3,7 @@ package net.underdesk.circolapp.viewmodels import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import net.underdesk.circolapp.data.CircularRepository +import net.underdesk.circolapp.shared.data.CircularRepository class RemindersViewModelFactory( private val circularRepository: CircularRepository, diff --git a/app/src/main/java/net/underdesk/circolapp/works/PollWork.kt b/app/src/main/java/net/underdesk/circolapp/works/PollWork.kt index 6eda6d0..b927143 100644 --- a/app/src/main/java/net/underdesk/circolapp/works/PollWork.kt +++ b/app/src/main/java/net/underdesk/circolapp/works/PollWork.kt @@ -33,10 +33,8 @@ import androidx.work.* import kotlinx.coroutines.coroutineScope import net.underdesk.circolapp.MainActivity import net.underdesk.circolapp.R -import net.underdesk.circolapp.data.AppDatabase -import net.underdesk.circolapp.data.Circular -import net.underdesk.circolapp.data.CircularRepository -import net.underdesk.circolapp.server.ServerAPI +import net.underdesk.circolapp.data.AndroidCircularRepository +import net.underdesk.circolapp.shared.data.Circular import java.util.concurrent.TimeUnit class PollWork(appContext: Context, workerParams: WorkerParameters) : @@ -93,10 +91,7 @@ class PollWork(appContext: Context, workerParams: WorkerParameters) : } override suspend fun doWork(): Result = coroutineScope { - val circularRepository = CircularRepository.getInstance( - AppDatabase.getInstance(applicationContext).circularDao(), - ServerAPI.getInstance(applicationContext) - ) + val circularRepository = AndroidCircularRepository.getInstance(applicationContext) val result = circularRepository.updateCirculars() if (!result.second) diff --git a/build.gradle.kts b/build.gradle.kts index eafba5c..e516748 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,9 @@ buildscript { dependencies { classpath(Config.Plugin.android) classpath(Config.Plugin.kotlin) + classpath(Config.Plugin.serialization) classpath(Config.Plugin.google) + classpath(Config.Plugin.sqlDelight) classpath(Config.Plugin.ktlint) classpath(Config.Plugin.aboutLibraries) // NOTE: Do not place your application dependencies here; they belong diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 6977d0a..856ce0b 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -1,10 +1,14 @@ object Config { object Plugin { - const val android = "com.android.tools.build:gradle:4.1.0" + const val android = "com.android.tools.build:gradle:4.1.1" const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Dependencies.Kotlin.version}" + const val serialization = + "org.jetbrains.kotlin:kotlin-serialization:${Dependencies.Kotlin.version}" const val google = "com.google.gms:google-services:4.3.4" const val ktlint = "org.jlleitschuh.gradle:ktlint-gradle:9.4.0" + const val sqlDelight = + "com.squareup.sqldelight:gradle-plugin:${Dependencies.SQLDelight.version}" const val aboutLibraries = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${Dependencies.AboutLibraries.version}" } diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 85b9fa5..87c7b7a 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -2,6 +2,8 @@ object Dependencies { object Kotlin { const val version = "1.4.10" const val core = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${version}" + const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9" + const val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" } object AndroidX { @@ -10,6 +12,7 @@ object Dependencies { const val constraintLayout = "androidx.constraintlayout:constraintlayout:2.0.3" const val swipeRefreshLayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" const val lifecycleExtensions = "androidx.lifecycle:lifecycle-extensions:2.2.0" + const val lifecycleLiveData = "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" const val preference = "androidx.preference:preference-ktx:1.1.1" private const val navigationVersion = "2.3.1" @@ -18,13 +21,6 @@ object Dependencies { const val navigationUi = "androidx.navigation:navigation-ui-ktx:${navigationVersion}" const val workManager = "androidx.work:work-runtime-ktx:2.4.0" - - object Room { - private const val version = "2.2.5" - const val roomRuntime = "androidx.room:room-runtime:${version}" - const val roomKtx = "androidx.room:room-ktx:${version}" - const val roomCompiler = "androidx.room:room-compiler:${version}" - } } object Google { @@ -36,12 +32,25 @@ object Dependencies { const val messaging = "com.google.firebase:firebase-messaging-ktx" } - object Square { - const val okhttp = "com.squareup.okhttp3:okhttp:4.8.1" + object Ktor { + private const val version = "1.4.1" + const val ktorCore = "io.ktor:ktor-client-core:$version" + const val ktorOkhttp = "io.ktor:ktor-client-okhttp:$version" + const val ktorIos = "io.ktor:ktor-client-ios:$version" + const val ktorJson = "io.ktor:ktor-client-json:$version" + const val ktorSerialization = "io.ktor:ktor-client-serialization:$version" + } - private const val moshiVersion = "1.9.3" - const val moshi = "com.squareup.moshi:moshi:${moshiVersion}" - const val moshiCodegen = "com.squareup.moshi:moshi-kotlin-codegen:${moshiVersion}" + object Serialization { + const val json = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1" + } + + object SQLDelight { + const val version = "1.4.4" + const val sqlDelightRuntime = "com.squareup.sqldelight:runtime:$version" + const val sqlDelightCoroutines = "com.squareup.sqldelight:coroutines-extensions:$version" + const val sqlDelightAndroid = "com.squareup.sqldelight:android-driver:$version" + const val sqlDelightNative = "com.squareup.sqldelight:native-driver:$version" } object AboutLibraries { diff --git a/gradle.properties b/gradle.properties index 23339e0..a280564 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,3 +19,8 @@ android.useAndroidX=true android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official +kotlin.mpp.stability.nowarn=true +kotlin.mpp.enableGranularSourceSetsMetadata=true +kotlin.native.enableDependencyPropagation=false + +xcodeproj=./ios/circolapp \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 9c1cbba..019e466 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,3 @@ +include(":shared") include(":app") rootProject.name = "Circolapp" diff --git a/shared/.gitignore b/shared/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/shared/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts new file mode 100644 index 0000000..80fc278 --- /dev/null +++ b/shared/build.gradle.kts @@ -0,0 +1,124 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") + id("com.android.library") + id("kotlin-android-extensions") + id("com.squareup.sqldelight") +} + +repositories { + gradlePluginPortal() + google() + jcenter() + mavenCentral() + maven { + url = uri("https://dl.bintray.com/kotlin/kotlin-eap") + } +} + +kotlin { + android() + + val sdkName = System.getenv("SDK_NAME") ?: "iphonesimulator" + + if (sdkName.startsWith("iphoneos")) { + iosArm64("ios") { + binaries { + framework { + baseName = "Shared" + } + } + } + } else { + iosX64("ios") { + binaries { + framework { + baseName = "Shared" + } + } + } + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(Dependencies.Kotlin.coroutinesCore) + + // Ktor + implementation(Dependencies.Ktor.ktorCore) + implementation(Dependencies.Ktor.ktorJson) + implementation(Dependencies.Ktor.ktorSerialization) + + // Serialization + implementation(Dependencies.Serialization.json) + + // SqlDelight + implementation(Dependencies.SQLDelight.sqlDelightRuntime) + implementation(Dependencies.SQLDelight.sqlDelightCoroutines) + } + } + val androidMain by getting { + dependencies { + implementation(Dependencies.Kotlin.coroutinesAndroid) + + // Ktor + implementation(Dependencies.Ktor.ktorOkhttp) + + // SqlDelight + implementation(Dependencies.SQLDelight.sqlDelightAndroid) + + // Misc + implementation(Dependencies.Misc.jsoup) + } + } + val iosMain by getting { + dependencies { + // Ktor + implementation(Dependencies.Ktor.ktorIos) + + // SqlDelight + implementation(Dependencies.SQLDelight.sqlDelightNative) + } + } + } +} + +android { + compileSdkVersion(Config.Android.compileSdk) + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + defaultConfig { + minSdkVersion(Config.Android.minSdk) + targetSdkVersion(Config.Android.targetSdk) + versionCode = 1 + versionName = "1.0" + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } +} + +sqldelight { + database("AppDatabase") { + packageName = "net.underdesk.circolapp.shared.data" + sourceFolders = listOf("sqldelight") + } +} + +val packForXcode by tasks.creating(Sync::class) { + group = "build" + val mode = System.getenv("CONFIGURATION") ?: "DEBUG" + val targetName = "ios" + val framework = + kotlin.targets.getByName(targetName).binaries.getFramework(mode) + inputs.property("mode", mode) + dependsOn(framework.linkTask) + val targetDir = File(buildDir, "xcode-frameworks") + from({ framework.outputDirectory }) + into(targetDir) +} + +tasks.getByName("build").dependsOn(packForXcode) diff --git a/shared/src/androidMain/AndroidManifest.xml b/shared/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..2e6f510 --- /dev/null +++ b/shared/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/PlatformDispatcher.kt b/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/PlatformDispatcher.kt new file mode 100644 index 0000000..13e9ef2 --- /dev/null +++ b/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/PlatformDispatcher.kt @@ -0,0 +1,7 @@ +package net.underdesk.circolapp.shared + +import kotlinx.coroutines.Dispatchers + +actual object PlatformDispatcher { + actual val IO = Dispatchers.IO +} diff --git a/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/data/DatabaseDriverFactory.kt b/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/data/DatabaseDriverFactory.kt new file mode 100644 index 0000000..3042675 --- /dev/null +++ b/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/data/DatabaseDriverFactory.kt @@ -0,0 +1,11 @@ +package net.underdesk.circolapp.shared.data + +import android.content.Context +import com.squareup.sqldelight.android.AndroidSqliteDriver +import com.squareup.sqldelight.db.SqlDriver + +actual class DatabaseDriverFactory(private val context: Context) { + actual fun createDriver(): SqlDriver { + return AndroidSqliteDriver(AppDatabase.Schema, context, "circolapp.db") + } +} diff --git a/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/server/KtorFactory.kt b/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/server/KtorFactory.kt new file mode 100644 index 0000000..f839100 --- /dev/null +++ b/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/server/KtorFactory.kt @@ -0,0 +1,18 @@ +package net.underdesk.circolapp.shared.server + +import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.features.json.* +import io.ktor.client.features.json.serializer.* + +actual class KtorFactory actual constructor() { + actual fun createClient() = HttpClient(OkHttp) { + install(JsonFeature) { + serializer = KotlinxSerializer( + kotlinx.serialization.json.Json { + ignoreUnknownKeys = true + } + ) + } + } +} diff --git a/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/server/curie/SpecificCurieServer.kt b/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/server/curie/SpecificCurieServer.kt new file mode 100644 index 0000000..a65379f --- /dev/null +++ b/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/server/curie/SpecificCurieServer.kt @@ -0,0 +1,24 @@ +package net.underdesk.circolapp.shared.server.curie + +import net.underdesk.circolapp.shared.data.Circular +import org.jsoup.Jsoup + +actual class SpecificCurieServer actual constructor(private val curieServer: CurieServer) { + actual fun parseHtml(string: String): List { + val document = Jsoup.parseBodyFragment(string) + val htmlList = document.getElementsByTag("ul")[0].getElementsByTag("a") + + val list = ArrayList() + + htmlList.forEach { element -> + if (element.parents().size == 6) { + list.last().attachmentsNames.add(element.text()) + list.last().attachmentsUrls.add(element.attr("href")) + } else if (element.parents().size == 4) { + list.add(curieServer.generateFromString(element.text(), element.attr("href"))) + } + } + + return list + } +} diff --git a/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/server/porporato/SpecificPorporatoServer.kt b/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/server/porporato/SpecificPorporatoServer.kt new file mode 100644 index 0000000..1b62e13 --- /dev/null +++ b/shared/src/androidMain/kotlin/net/underdesk/circolapp/shared/server/porporato/SpecificPorporatoServer.kt @@ -0,0 +1,27 @@ +package net.underdesk.circolapp.shared.server.porporato + +import net.underdesk.circolapp.shared.data.Circular +import org.jsoup.Jsoup + +actual class SpecificPorporatoServer actual constructor(private val porporatoServer: PorporatoServer) { + actual fun parseHtml(string: String): List { + val document = Jsoup.parseBodyFragment(string) + val htmlList = document.getElementsByTag("table")[2] + .getElementsByTag("td")[2] + .getElementsByTag("a") + + val list = ArrayList() + + for (i in 0 until htmlList.size) { + list.add( + porporatoServer.generateFromString( + htmlList[i].text(), + htmlList[i].attr("href"), + i.toLong() + ) + ) + } + + return list + } +} diff --git a/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/PlatformDispatcher.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/PlatformDispatcher.kt new file mode 100644 index 0000000..a24f8b9 --- /dev/null +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/PlatformDispatcher.kt @@ -0,0 +1,7 @@ +package net.underdesk.circolapp.shared + +import kotlinx.coroutines.CoroutineDispatcher + +expect object PlatformDispatcher { + val IO: CoroutineDispatcher +} diff --git a/app/src/main/java/net/underdesk/circolapp/data/Circular.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/Circular.kt similarity index 82% rename from app/src/main/java/net/underdesk/circolapp/data/Circular.kt rename to shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/Circular.kt index f7b1705..44eecf4 100644 --- a/app/src/main/java/net/underdesk/circolapp/data/Circular.kt +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/Circular.kt @@ -16,14 +16,8 @@ * along with this program. If not, see . */ -package net.underdesk.circolapp.data +package net.underdesk.circolapp.shared.data -import android.os.Parcelable -import androidx.room.Entity -import kotlinx.android.parcel.Parcelize - -@Parcelize -@Entity(tableName = "circulars", primaryKeys = ["id", "school"]) data class Circular( val id: Long, val school: Int, @@ -34,4 +28,4 @@ data class Circular( var reminder: Boolean = false, val attachmentsNames: MutableList = mutableListOf(), val attachmentsUrls: MutableList = mutableListOf() -) : Parcelable +) diff --git a/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularDao.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularDao.kt new file mode 100644 index 0000000..4ab6fa7 --- /dev/null +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularDao.kt @@ -0,0 +1,87 @@ +package net.underdesk.circolapp.shared.data + +import com.squareup.sqldelight.runtime.coroutines.asFlow +import com.squareup.sqldelight.runtime.coroutines.mapToList +import kotlinx.coroutines.withContext +import net.underdesk.circolapp.shared.PlatformDispatcher +import net.underdesk.circolapp.shared.utils.SqlUtils.joinToString +import net.underdesk.circolapp.shared.utils.SqlUtils.toBoolean +import net.underdesk.circolapp.shared.utils.SqlUtils.toList +import net.underdesk.circolapp.shared.utils.SqlUtils.toLong + +class CircularDao( + database: AppDatabase +) { + private val appDatabaseQueries = database.appDatabaseQueries + + private val circularMapper = + { id: Long, school: Long, name: String, url: String, date: String, favourite: Long, reminder: Long, attachmentsNames: String, attachmentsUrls: String -> + Circular( + id, + school.toInt(), + name, + url, + date, + favourite.toBoolean(), + reminder.toBoolean(), + attachmentsNames.toList(), + attachmentsUrls.toList() + ) + } + + suspend fun insertAll(circulars: List) = withContext(PlatformDispatcher.IO) { + circulars.forEach { + appDatabaseQueries.insertCircular( + it.id, + it.school.toLong(), + it.name, + it.url, + it.date, + it.favourite.toLong(), + it.reminder.toLong(), + it.attachmentsNames.joinToString(), + it.attachmentsUrls.joinToString() + ) + } + } + + suspend fun update(id: Long, school: Int, favourite: Boolean, reminder: Boolean) = + withContext(PlatformDispatcher.IO) { + appDatabaseQueries.updateCircular( + favourite.toLong(), + reminder.toLong(), + id, + school.toLong() + ) + } + + suspend fun deleteAll() = withContext(PlatformDispatcher.IO) { + appDatabaseQueries.deleteAllCirculars() + } + + fun getCircular(id: Long, school: Int) = appDatabaseQueries.getCircular(id, school.toLong(), circularMapper).executeAsOne() + + fun getCirculars(school: Int) = + appDatabaseQueries.getCirculars(school.toLong(), circularMapper).executeAsList() + + fun getFlowCirculars(school: Int) = + appDatabaseQueries.getCirculars(school.toLong(), circularMapper).asFlow().mapToList() + + fun searchCirculars(query: String, school: Int) = + appDatabaseQueries.searchCirculars(school.toLong(), query, circularMapper).asFlow() + .mapToList() + + fun getFavourites(school: Int) = + appDatabaseQueries.getFavourites(school.toLong(), circularMapper).asFlow().mapToList() + + fun searchFavourites(query: String, school: Int) = + appDatabaseQueries.searchFavourites(school.toLong(), query, circularMapper).asFlow() + .mapToList() + + fun getReminders(school: Int) = + appDatabaseQueries.getReminders(school.toLong(), circularMapper).asFlow().mapToList() + + fun searchReminders(query: String, school: Int) = + appDatabaseQueries.searchReminders(school.toLong(), query, circularMapper).asFlow() + .mapToList() +} diff --git a/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularRepository.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularRepository.kt new file mode 100644 index 0000000..8d485cb --- /dev/null +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularRepository.kt @@ -0,0 +1,36 @@ +package net.underdesk.circolapp.shared.data + +import net.underdesk.circolapp.shared.server.ServerAPI + +class CircularRepository( + val circularDao: CircularDao, + private val serverAPI: ServerAPI +) { + suspend fun updateCirculars(returnNewCirculars: Boolean = true): Pair, Boolean> { + var onlyNewCirculars = listOf() + + val result = serverAPI.getCircularsFromServer() + if (result.second == ServerAPI.Companion.Result.ERROR) + return Pair(emptyList(), false) + + val oldCirculars = circularDao.getCirculars(serverAPI.serverID()) + val newCirculars = result.first + + if (newCirculars.size != oldCirculars.size) { + if (newCirculars.size < oldCirculars.size) { + circularDao.deleteAll() + } + + if (returnNewCirculars) { + val oldCircularsSize = + if (newCirculars.size < oldCirculars.size) 0 else oldCirculars.size + + val circularCount = newCirculars.size - oldCircularsSize + onlyNewCirculars = newCirculars.subList(0, circularCount) + } + + circularDao.insertAll(newCirculars) + } + return Pair(onlyNewCirculars, true) + } +} diff --git a/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/DatabaseDriverFactory.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/DatabaseDriverFactory.kt new file mode 100644 index 0000000..591f4fc --- /dev/null +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/DatabaseDriverFactory.kt @@ -0,0 +1,7 @@ +package net.underdesk.circolapp.shared.data + +import com.squareup.sqldelight.db.SqlDriver + +expect class DatabaseDriverFactory { + fun createDriver(): SqlDriver +} diff --git a/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/KtorFactory.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/KtorFactory.kt new file mode 100644 index 0000000..daf1ac1 --- /dev/null +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/KtorFactory.kt @@ -0,0 +1,7 @@ +package net.underdesk.circolapp.shared.server + +import io.ktor.client.* + +expect class KtorFactory() { + fun createClient(): HttpClient +} diff --git a/app/src/main/java/net/underdesk/circolapp/server/Server.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/Server.kt similarity index 90% rename from app/src/main/java/net/underdesk/circolapp/server/Server.kt rename to shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/Server.kt index 18aa92c..9229047 100644 --- a/app/src/main/java/net/underdesk/circolapp/server/Server.kt +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/Server.kt @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -package net.underdesk.circolapp.server +package net.underdesk.circolapp.shared.server -import net.underdesk.circolapp.data.Circular +import net.underdesk.circolapp.shared.data.Circular abstract class Server { abstract val serverID: Int diff --git a/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/ServerAPI.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/ServerAPI.kt new file mode 100644 index 0000000..7ff8966 --- /dev/null +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/ServerAPI.kt @@ -0,0 +1,71 @@ +/* + * Circolapp + * Copyright (C) 2019-2020 Matteo Schiff + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.underdesk.circolapp.shared.server + +import kotlinx.coroutines.withContext +import net.underdesk.circolapp.shared.PlatformDispatcher +import net.underdesk.circolapp.shared.data.Circular +import net.underdesk.circolapp.shared.server.curie.CurieServer +import net.underdesk.circolapp.shared.server.porporato.PorporatoServer + +class ServerAPI( + private var server: Server +) { + fun serverID(): Int = server.serverID + + suspend fun getCircularsFromServer(): Pair, Result> = withContext(PlatformDispatcher.IO) { + val newCircularsAvailable = server.newCircularsAvailable() + + if (newCircularsAvailable.second == Result.ERROR) + return@withContext Pair(emptyList(), Result.ERROR) + + if (!newCircularsAvailable.first) + return@withContext Pair(emptyList(), Result.SUCCESS) + + server.getCircularsFromServer() + } + + fun changeServer(server: Server) { + this.server = server + } + + companion object { + enum class Servers { + CURIE, PORPORATO + } + + enum class Result { + SUCCESS, ERROR + } + + fun getServerId(server: Servers): Int { + return Servers.values().indexOf(server) + } + + fun getServerName(server: Servers) = when (server) { + Servers.CURIE -> "Liceo scientifico Maria Curie" + Servers.PORPORATO -> "Liceo G.F. Porporato" + } + + fun createServer(server: Servers) = when (server) { + Servers.CURIE -> CurieServer() + Servers.PORPORATO -> PorporatoServer() + } + } +} diff --git a/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/curie/CurieServer.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/curie/CurieServer.kt new file mode 100644 index 0000000..cfe9e8d --- /dev/null +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/curie/CurieServer.kt @@ -0,0 +1,73 @@ +package net.underdesk.circolapp.shared.server.curie + +import io.ktor.client.request.* +import io.ktor.utils.io.errors.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.underdesk.circolapp.shared.data.Circular +import net.underdesk.circolapp.shared.server.KtorFactory +import net.underdesk.circolapp.shared.server.Server +import net.underdesk.circolapp.shared.server.ServerAPI +import net.underdesk.circolapp.shared.server.pojo.Response +import kotlin.coroutines.cancellation.CancellationException + +class CurieServer : Server() { + private val client = KtorFactory().createClient() + + override val serverID = ServerAPI.getServerId(ServerAPI.Companion.Servers.CURIE) + + override suspend fun getCircularsFromServer(): Pair, ServerAPI.Companion.Result> { + return try { + withContext(Dispatchers.Default) { + val json = retrieveDataFromServer() + val list = SpecificCurieServer(this@CurieServer).parseHtml(json.content.rendered) + Pair(list, ServerAPI.Companion.Result.SUCCESS) + } + } catch (exception: IOException) { + Pair(emptyList(), ServerAPI.Companion.Result.ERROR) + } + } + + override suspend fun newCircularsAvailable(): Pair { + return Pair(true, ServerAPI.Companion.Result.SUCCESS) + } + + @OptIn(ExperimentalStdlibApi::class) + @Throws(IOException::class, CancellationException::class) + private suspend fun retrieveDataFromServer(): Response { + return client.get(ENDPOINT_URL) + } + + fun generateFromString(string: String, url: String): Circular { + val idRegex = + """(\d+)""".toRegex() + val idMatcher = idRegex.find(string) + + val id = idMatcher?.value?.toLong() ?: -1L + + val dateRegex = + """(\d{2}/\d{2}/\d{4})""".toRegex() + val dateMatcher = dateRegex.find(string) + + var title = string.removeSuffix("-signed") + + return if (dateMatcher != null) { + title = title.removeRange(0, dateMatcher.range.last + 1) + .removePrefix(" ") + .removePrefix("_") + .removePrefix(" ") + + Circular(id, serverID, title, url, dateMatcher.value) + } else { + Circular(id, serverID, title, url, "") + } + } + + companion object { + const val ENDPOINT_URL = "https://www.curiepinerolo.edu.it/wp-json/wp/v2/pages/5958" + } +} + +expect class SpecificCurieServer(curieServer: CurieServer) { + fun parseHtml(string: String): List +} diff --git a/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/pojo/WordpressResponse.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/pojo/WordpressResponse.kt new file mode 100644 index 0000000..e180583 --- /dev/null +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/pojo/WordpressResponse.kt @@ -0,0 +1,13 @@ +package net.underdesk.circolapp.shared.server.pojo + +import kotlinx.serialization.Serializable + +@Serializable +data class Response( + val content: Content +) + +@Serializable +data class Content( + val rendered: String +) diff --git a/app/src/main/java/net/underdesk/circolapp/server/porporato/PorporatoServer.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/porporato/PorporatoServer.kt similarity index 67% rename from app/src/main/java/net/underdesk/circolapp/server/porporato/PorporatoServer.kt rename to shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/porporato/PorporatoServer.kt index d3b6397..4c0a8b5 100644 --- a/app/src/main/java/net/underdesk/circolapp/server/porporato/PorporatoServer.kt +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/server/porporato/PorporatoServer.kt @@ -1,18 +1,19 @@ -package net.underdesk.circolapp.server.porporato +package net.underdesk.circolapp.shared.server.porporato +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.utils.io.charsets.* +import io.ktor.utils.io.errors.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import net.underdesk.circolapp.data.Circular -import net.underdesk.circolapp.server.Server -import net.underdesk.circolapp.server.ServerAPI -import okhttp3.OkHttpClient -import okhttp3.Request -import org.jsoup.Jsoup -import java.io.IOException -import java.util.regex.Pattern +import net.underdesk.circolapp.shared.data.Circular +import net.underdesk.circolapp.shared.server.KtorFactory +import net.underdesk.circolapp.shared.server.Server +import net.underdesk.circolapp.shared.server.ServerAPI +import kotlin.coroutines.cancellation.CancellationException class PorporatoServer : Server() { - private val client = OkHttpClient() + private val client = KtorFactory().createClient() private val baseUrl = "https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/" private val endpointUrls = listOf( @@ -52,27 +53,13 @@ class PorporatoServer : Server() { return Pair(true, ServerAPI.Companion.Result.SUCCESS) } - @Throws(IOException::class) + @OptIn(ExperimentalStdlibApi::class) + @Throws(IOException::class, CancellationException::class) private suspend fun parsePage(url: String): List { val response = retrieveDataFromServer(url) return withContext(Dispatchers.Default) { - val document = Jsoup.parseBodyFragment(response) - val htmlList = document.getElementsByTag("table")[2] - .getElementsByTag("td")[2] - .getElementsByTag("a") - - val list = ArrayList() - - for (i in 0 until htmlList.size) { - list.add( - generateFromString( - htmlList[i].text(), - htmlList[i].attr("href"), - i.toLong() - ) - ) - } + val list = SpecificPorporatoServer(this@PorporatoServer).parseHtml(response).toMutableList() // Identify and group all attachments list.removeAll { attachment -> @@ -109,52 +96,45 @@ class PorporatoServer : Server() { } } - @Throws(IOException::class) + @OptIn(ExperimentalStdlibApi::class) + @Throws(IOException::class, CancellationException::class) private suspend fun retrieveDataFromServer(url: String): String { - val request = Request.Builder() - .url(url) - .build() - - return withContext(Dispatchers.IO) { - val response = client.newCall(request).execute() - - if (!response.isSuccessful) { - throw IOException("HTTP error code: ${response.code})") - } - - response.body!!.string() - } + return client.request(url).readText(Charsets.ISO_8859_1) } - private fun generateFromString(string: String, path: String, index: Long): Circular { + fun generateFromString(string: String, path: String, index: Long): Circular { val fullUrl = baseUrl + path var title = string val idRegex = - """(\d+)""" - val matcherId = Pattern.compile(idRegex).matcher(string) - val id = if (!string.startsWith("Avviso") && matcherId.find()) { - title = title.removeRange(matcherId.start(), matcherId.end()) + """(\d+)""".toRegex() + val idMatcher = idRegex.find(string) + val id = if (!string.startsWith("Avviso") && idMatcher != null) { + title = title.removeRange(idMatcher.range) .removePrefix(" ") .removePrefix("-") .removePrefix(" ") - matcherId.group(1)?.toLong() ?: -index + idMatcher.value.toLong() } else { -index } val dateRegex = - """(\d{2}-\d{2}-\d{4})""" - val matcherDate = Pattern.compile(dateRegex).matcher(title) + """(\d{2}-\d{2}-\d{4})""".toRegex() + val dateMatcher = dateRegex.find(title) - return if (matcherDate.find()) { - title = title.removeRange(matcherDate.start(), matcherDate.end()) + return if (dateMatcher != null) { + title = title.removeRange(dateMatcher.range) .removeSuffix(" (pubb.: )") - Circular(id, serverID, title, fullUrl, matcherDate.group(1)?.replace("-", "/") ?: "") + Circular(id, serverID, title, fullUrl, dateMatcher.value.replace("-", "/")) } else { Circular(id, serverID, title, fullUrl, "") } } } + +expect class SpecificPorporatoServer(porporatoServer: PorporatoServer) { + fun parseHtml(string: String): List +} diff --git a/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/utils/SqlUtils.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/utils/SqlUtils.kt new file mode 100644 index 0000000..3819e74 --- /dev/null +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/utils/SqlUtils.kt @@ -0,0 +1,29 @@ +package net.underdesk.circolapp.shared.utils + +object SqlUtils { + fun Boolean.toLong() = if (this) 1L else 0L + + fun Long.toBoolean() = this == 1L + + fun String?.toList(): MutableList { + val list: MutableList = mutableListOf() + + if (this != null) { + for (attachment in this.split("˜")) { + list.add(attachment) + } + } + + return list.dropLast(1).toMutableList() + } + + fun List.joinToString(): String { + var string = "" + + for (attachment in this) { + string += "$attachment˜" + } + + return string + } +} diff --git a/shared/src/commonMain/sqldelight/net.underdesk.circolapp.shared.data/AppDatabase.sq b/shared/src/commonMain/sqldelight/net.underdesk.circolapp.shared.data/AppDatabase.sq new file mode 100644 index 0000000..656f335 --- /dev/null +++ b/shared/src/commonMain/sqldelight/net.underdesk.circolapp.shared.data/AppDatabase.sq @@ -0,0 +1,59 @@ +CREATE TABLE Circulars ( + id INTEGER NOT NULL, + school INTEGER NOT NULL, + name TEXT NOT NULL, + url TEXT NOT NULL, + date TEXT NOT NULL, + favourite INTEGER NOT NULL DEFAULT 0, + reminder INTEGER NOT NULL DEFAULT 0, + attachmentsNames TEXT NOT NULL, + attachmentsUrls TEXT NOT NULL, + PRIMARY KEY (id, school) +); + +insertCircular: +INSERT OR IGNORE INTO Circulars(id, school, name, url, date, favourite, reminder, attachmentsNames, attachmentsUrls) +VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?); + +updateCircular: +UPDATE Circulars +SET favourite = ?, reminder = ? +WHERE id = ? AND school = ?; + +deleteAllCirculars: +DELETE FROM Circulars; + +getCirculars: +SELECT * FROM Circulars +WHERE school IS ? +ORDER BY id DESC; + +getCircular: +SELECT * FROM Circulars +WHERE id IS ? AND school = ? +ORDER BY id DESC; + +searchCirculars: +SELECT * FROM Circulars +WHERE school IS ? AND name LIKE ? +ORDER BY id DESC; + +getFavourites: +SELECT * FROM Circulars +WHERE school IS ? AND favourite +ORDER BY id DESC; + +searchFavourites: +SELECT * FROM Circulars +WHERE school IS ? AND favourite AND name LIKE ? +ORDER BY id DESC; + +getReminders: +SELECT * FROM Circulars +WHERE school IS ? AND reminder +ORDER BY id DESC; + +searchReminders: +SELECT * FROM Circulars +WHERE school IS ? AND reminder AND name LIKE ? +ORDER BY id DESC; diff --git a/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/PlatformDispatcher.kt b/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/PlatformDispatcher.kt new file mode 100644 index 0000000..dd5fa51 --- /dev/null +++ b/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/PlatformDispatcher.kt @@ -0,0 +1,7 @@ +package net.underdesk.circolapp.shared + +import kotlinx.coroutines.Dispatchers + +actual object PlatformDispatcher { + actual val IO = Dispatchers.Default +} diff --git a/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/data/DatabaseDriverFactory.kt b/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/data/DatabaseDriverFactory.kt new file mode 100644 index 0000000..352474a --- /dev/null +++ b/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/data/DatabaseDriverFactory.kt @@ -0,0 +1,10 @@ +package net.underdesk.circolapp.shared.data + +import com.squareup.sqldelight.db.SqlDriver +import com.squareup.sqldelight.drivers.native.NativeSqliteDriver + +actual class DatabaseDriverFactory { + actual fun createDriver(): SqlDriver { + return NativeSqliteDriver(AppDatabase.Schema, "circolapp.db") + } +} diff --git a/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/server/KtorFactory.kt b/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/server/KtorFactory.kt new file mode 100644 index 0000000..55b1673 --- /dev/null +++ b/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/server/KtorFactory.kt @@ -0,0 +1,18 @@ +package net.underdesk.circolapp.shared.server + +import io.ktor.client.* +import io.ktor.client.engine.ios.* +import io.ktor.client.features.json.* +import io.ktor.client.features.json.serializer.* + +actual class KtorFactory actual constructor() { + actual fun createClient() = HttpClient(Ios) { + install(JsonFeature) { + serializer = KotlinxSerializer( + kotlinx.serialization.json.Json { + ignoreUnknownKeys = true + } + ) + } + } +} diff --git a/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/server/curie/SpecificCurieServer.kt b/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/server/curie/SpecificCurieServer.kt new file mode 100644 index 0000000..abd2175 --- /dev/null +++ b/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/server/curie/SpecificCurieServer.kt @@ -0,0 +1,9 @@ +package net.underdesk.circolapp.shared.server.curie + +import net.underdesk.circolapp.shared.data.Circular + +actual class SpecificCurieServer actual constructor(val curieServer: CurieServer) { + actual fun parseHtml(string: String): List { + TODO("Not yet implemented") + } +} diff --git a/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/server/porporato/SpecificPorporatoServer.kt b/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/server/porporato/SpecificPorporatoServer.kt new file mode 100644 index 0000000..3a0d198 --- /dev/null +++ b/shared/src/iosMain/kotlin/net/underdesk/circolapp/shared/server/porporato/SpecificPorporatoServer.kt @@ -0,0 +1,9 @@ +package net.underdesk.circolapp.shared.server.porporato + +import net.underdesk.circolapp.shared.data.Circular + +actual class SpecificPorporatoServer actual constructor(porporatoServer: PorporatoServer) { + actual fun parseHtml(string: String): List { + TODO("Not yet implemented") + } +}