Add shared module

This commit is contained in:
2020-11-12 10:25:02 +01:00
parent 151b4dbc76
commit 6ab63a7ddb
57 changed files with 929 additions and 617 deletions

View File

@@ -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)

View File

@@ -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,10 +43,9 @@ class AlarmBroadcastReceiver : BroadcastReceiver() {
}
override fun onReceive(context: Context, intent: Intent) {
object : Thread() {
override fun run() {
GlobalScope.launch {
createNotificationChannel(context)
val circular = AppDatabase.getInstance(context).circularDao().getCircular(
val circular = AndroidDatabase.getDaoInstance(context).getCircular(
intent.getLongExtra(
CIRCULAR_ID,
0
@@ -58,10 +59,9 @@ class AlarmBroadcastReceiver : BroadcastReceiver() {
context,
circular
)
AppDatabase.getInstance(context).circularDao()
.update(circular.apply { reminder = false })
AndroidDatabase.getDaoInstance(context)
.update(circular.id, circular.school, circular.favourite, false)
}
}.start()
}
private fun createNotification(context: Context, circular: Circular) {

View File

@@ -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())

View File

@@ -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<Circular>,
private val adapterCallback: AdapterCallback
private val adapterCallback: AdapterCallback,
private val adapterScope: CoroutineScope
) :
RecyclerView.Adapter<CircularLetterAdapter.CircularLetterViewHolder>() {
private lateinit var context: Context
@@ -158,18 +161,20 @@ class CircularLetterAdapter(
}
holder.favouriteButton.setOnClickListener {
object : Thread() {
override fun run() {
AppDatabase.getInstance(context).circularDao()
.update(circulars[position].apply { favourite = !favourite })
adapterScope.launch {
AndroidDatabase.getDaoInstance(context).update(
circulars[position].id,
circulars[position].school,
!circulars[position].favourite,
circulars[position].reminder
)
}
}.start()
}
holder.reminderButton.setOnClickListener {
if (circulars[position].reminder) {
object : Thread() {
override fun run() {
adapterScope.launch {
val pendingIntent = PendingIntent.getBroadcast(
context,
circulars[position].id.toInt(),
@@ -179,10 +184,14 @@ class CircularLetterAdapter(
pendingIntent.cancel()
AppDatabase.getInstance(context).circularDao()
.update(circulars[position].apply { reminder = false })
AndroidDatabase.getDaoInstance(context)
.update(
circulars[position].id,
circulars[position].school,
circulars[position].favourite,
false
)
}
}.start()
} else {
NewReminderFragment.create(circulars[position])
.show((context as FragmentActivity).supportFragmentManager, "NewReminderDialog")

View File

@@ -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))
}

View File

@@ -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<String> {
val list: MutableList<String> = 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>): String {
var string = ""
for (attachment in list) {
string += "$attachment˜"
}
return string
fun getDaoInstance(context: Context): CircularDao {
return CircularDao(getInstance(context))
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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 }
}
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Circular>
@Query("SELECT * FROM circulars WHERE school is :school ORDER BY id DESC")
fun getLiveCirculars(school: Int): LiveData<List<Circular>>
@Query("SELECT * FROM circulars WHERE school is :school AND name LIKE :query ORDER BY id DESC")
fun searchCirculars(query: String, school: Int): LiveData<List<Circular>>
@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<List<Circular>>
@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<List<Circular>>
@Query("SELECT * FROM circulars WHERE school is :school AND reminder ORDER BY id DESC")
fun getReminders(school: Int): LiveData<List<Circular>>
@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<List<Circular>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertAll(circulars: List<Circular>)
@Update
fun update(circular: Circular)
@Query("DELETE FROM circulars")
fun deleteAll()
}

View File

@@ -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<List<Circular>, Boolean> {
var onlyNewCirculars = listOf<Circular>()
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 }
}
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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,12 +98,11 @@ class NewReminderFragment : DialogFragment() {
minute
)
object : Thread() {
override fun run() {
lifecycleScope.launch {
context?.let { context ->
circular?.let { circular ->
AppDatabase.getInstance(context).circularDao()
.update(circular.apply { reminder = true })
circular.let { circular ->
AndroidDatabase.getDaoInstance(context)
.update(circular.id, circular.school, circular.favourite, true)
val pendingIntent = PendingIntent.getBroadcast(
context,
@@ -127,7 +123,6 @@ class NewReminderFragment : DialogFragment() {
}
dismiss()
}
}.start()
}
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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))
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<List<Circular>, 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()
}
}
}

View File

@@ -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<List<Circular>, 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<Circular>()
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<Boolean, ServerAPI.Companion.Result> {
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"
}
}

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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<List<Circular>> =
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()
}
}

View File

@@ -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,

View File

@@ -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<List<Circular>> =
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()
}
}

View File

@@ -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,

View File

@@ -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<List<Circular>> =
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()
}
}

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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}"
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -1,2 +1,3 @@
include(":shared")
include(":app")
rootProject.name = "Circolapp"

1
shared/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

124
shared/build.gradle.kts Normal file
View File

@@ -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<KotlinNativeTarget>(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)

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="net.underdesk.circolapp.shared.android" />

View File

@@ -0,0 +1,7 @@
package net.underdesk.circolapp.shared
import kotlinx.coroutines.Dispatchers
actual object PlatformDispatcher {
actual val IO = Dispatchers.IO
}

View File

@@ -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")
}
}

View File

@@ -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
}
)
}
}
}

View File

@@ -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<Circular> {
val document = Jsoup.parseBodyFragment(string)
val htmlList = document.getElementsByTag("ul")[0].getElementsByTag("a")
val list = ArrayList<Circular>()
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
}
}

View File

@@ -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<Circular> {
val document = Jsoup.parseBodyFragment(string)
val htmlList = document.getElementsByTag("table")[2]
.getElementsByTag("td")[2]
.getElementsByTag("a")
val list = ArrayList<Circular>()
for (i in 0 until htmlList.size) {
list.add(
porporatoServer.generateFromString(
htmlList[i].text(),
htmlList[i].attr("href"),
i.toLong()
)
)
}
return list
}
}

View File

@@ -0,0 +1,7 @@
package net.underdesk.circolapp.shared
import kotlinx.coroutines.CoroutineDispatcher
expect object PlatformDispatcher {
val IO: CoroutineDispatcher
}

View File

@@ -16,14 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<String> = mutableListOf(),
val attachmentsUrls: MutableList<String> = mutableListOf()
) : Parcelable
)

View File

@@ -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<Circular>) = 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()
}

View File

@@ -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<List<Circular>, Boolean> {
var onlyNewCirculars = listOf<Circular>()
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)
}
}

View File

@@ -0,0 +1,7 @@
package net.underdesk.circolapp.shared.data
import com.squareup.sqldelight.db.SqlDriver
expect class DatabaseDriverFactory {
fun createDriver(): SqlDriver
}

View File

@@ -0,0 +1,7 @@
package net.underdesk.circolapp.shared.server
import io.ktor.client.*
expect class KtorFactory() {
fun createClient(): HttpClient
}

View File

@@ -16,9 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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<List<Circular>, 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()
}
}
}

View File

@@ -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<List<Circular>, 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<Boolean, ServerAPI.Companion.Result> {
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<Circular>
}

View File

@@ -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
)

View File

@@ -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<Circular> {
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<Circular>()
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})")
return client.request<HttpResponse>(url).readText(Charsets.ISO_8859_1)
}
response.body!!.string()
}
}
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<Circular>
}

View File

@@ -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<String> {
val list: MutableList<String> = mutableListOf()
if (this != null) {
for (attachment in this.split("˜")) {
list.add(attachment)
}
}
return list.dropLast(1).toMutableList()
}
fun List<String>.joinToString(): String {
var string = ""
for (attachment in this) {
string += "$attachment˜"
}
return string
}
}

View File

@@ -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;

View File

@@ -0,0 +1,7 @@
package net.underdesk.circolapp.shared
import kotlinx.coroutines.Dispatchers
actual object PlatformDispatcher {
actual val IO = Dispatchers.Default
}

View File

@@ -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")
}
}

View File

@@ -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
}
)
}
}
}

View File

@@ -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<Circular> {
TODO("Not yet implemented")
}
}

View File

@@ -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<Circular> {
TODO("Not yet implemented")
}
}