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

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

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,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")

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,37 +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.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,
val name: String,
val url: String,
val date: String,
var favourite: Boolean = false,
var reminder: Boolean = false,
val attachmentsNames: MutableList<String> = mutableListOf(),
val attachmentsUrls: MutableList<String> = mutableListOf()
) : Parcelable

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,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()
}
}
}

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,27 +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 net.underdesk.circolapp.data.Circular
abstract class Server {
abstract val serverID: Int
abstract suspend fun getCircularsFromServer(): Pair<List<Circular>, ServerAPI.Companion.Result>
abstract suspend fun newCircularsAvailable(): Pair<Boolean, ServerAPI.Companion.Result>
}

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

@@ -1,160 +0,0 @@
package net.underdesk.circolapp.server.porporato
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
class PorporatoServer : Server() {
private val client = OkHttpClient()
private val baseUrl = "https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/"
private val endpointUrls = listOf(
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-01-Settembre/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-02-Ottobre/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-03-Novembre/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-04-Dicembre/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-05-Gennaio/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-06-Febbraio/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-07-Marzo/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-08-Aprile/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-09-Maggio/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-10-Giugno/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-11-Luglio/",
"https://www.liceoporporato.edu.it/ARCHIVIO/PR/VP/circolari.php?dirname=CIRCOLARIP/- CIRCOLARI 2020-21/-12-Agosto/"
)
override val serverID = ServerAPI.getServerId(ServerAPI.Companion.Servers.PORPORATO)
override suspend fun getCircularsFromServer(): Pair<List<Circular>, ServerAPI.Companion.Result> {
return try {
val list = arrayListOf<Circular>()
for (url in endpointUrls) {
list.addAll(parsePage(url))
}
list.sortByDescending { it.id }
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 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()
)
)
}
// Identify and group all attachments
list.removeAll { attachment ->
if (attachment.name.startsWith("All", true)) {
val parent = list.find { it.id == attachment.id && !it.name.startsWith("All") }
parent?.attachmentsNames?.add(attachment.name)
parent?.attachmentsUrls?.add(attachment.url)
return@removeAll true
}
false
}
// Identify and group attachments not marked with "All"
var lastIndex = -1
var lastId = -1L
list.removeAll { attachment ->
if (lastId == attachment.id) {
val parent = list[lastIndex]
parent.attachmentsNames.add(attachment.name)
parent.attachmentsUrls.add(attachment.url)
return@removeAll true
}
lastId = attachment.id
lastIndex = list.indexOf(attachment)
false
}
list
}
}
@Throws(IOException::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()
}
}
private 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())
.removePrefix(" ")
.removePrefix("-")
.removePrefix(" ")
matcherId.group(1)?.toLong() ?: -index
} else {
-index
}
val dateRegex =
"""(\d{2}-\d{2}-\d{4})"""
val matcherDate = Pattern.compile(dateRegex).matcher(title)
return if (matcherDate.find()) {
title = title.removeRange(matcherDate.start(), matcherDate.end())
.removeSuffix(" (pubb.: )")
Circular(id, serverID, title, fullUrl, matcherDate.group(1)?.replace("-", "/") ?: "")
} else {
Circular(id, serverID, title, fullUrl, "")
}
}
}

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)