Add admin messages (#1553)

This commit is contained in:
Rafał Borcz 2021-10-13 23:58:24 +02:00 committed by GitHub
parent d6918077bf
commit e3122127c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 2868 additions and 70 deletions

View File

@ -66,12 +66,14 @@ android {
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
}
debug {
resValue "string", "app_name", "Wulkanowy DEV " + defaultConfig.versionCode
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
ext.enableCrashlytics = project.hasProperty("enableFirebase")
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
}
}
@ -170,7 +172,7 @@ ext {
}
dependencies {
implementation "io.github.wulkanowy:sdk:1.3.0"
implementation "io.github.wulkanowy:sdk:4efd64264b"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
@ -212,6 +214,10 @@ dependencies {
implementation 'com.github.ncapdevi:FragNav:3.3.0'
implementation "com.github.YarikSOffice:lingver:1.3.0"
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.2"
implementation "com.jakewharton.timber:timber:5.0.1"
implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation 'com.github.bastienpaulfr:Treessence:1.0.5'
@ -234,7 +240,7 @@ dependencies {
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker"
debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:v1.0.6'
debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:1.0.6'
testImplementation "junit:junit:4.13.2"
testImplementation "io.mockk:mockk:$mockk"

File diff suppressed because it is too large Load Diff

View File

@ -2,48 +2,49 @@ package io.github.wulkanowy.data
import android.content.Context
import android.content.SharedPreferences
import android.content.res.AssetManager
import android.content.res.Resources
import androidx.preference.PreferenceManager
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.chuckerteam.chucker.api.RetentionManager
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.create
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal class RepositoryModule {
internal class DataModule {
@Singleton
@Provides
fun provideSdk(chuckerCollector: ChuckerCollector, @ApplicationContext context: Context): Sdk {
return Sdk().apply {
fun provideSdk(chuckerInterceptor: ChuckerInterceptor) =
Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL
setSimpleHttpLogger { Timber.d(it) }
// for debug only
addInterceptor(
ChuckerInterceptor.Builder(context)
.collector(chuckerCollector)
.alwaysReadResponseBody(true)
.build(), network = true
)
}
addInterceptor(chuckerInterceptor, network = true)
}
@Singleton
@ -51,13 +52,50 @@ internal class RepositoryModule {
fun provideChuckerCollector(
@ApplicationContext context: Context,
prefRepository: PreferencesRepository
): ChuckerCollector {
return ChuckerCollector(
) = ChuckerCollector(
context = context,
showNotification = prefRepository.isDebugNotificationEnable,
retentionPeriod = RetentionManager.Period.ONE_HOUR
)
}
@Singleton
@Provides
fun provideChuckerInterceptor(
@ApplicationContext context: Context,
chuckerCollector: ChuckerCollector
) = ChuckerInterceptor.Builder(context)
.collector(chuckerCollector)
.alwaysReadResponseBody(true)
.build()
@Singleton
@Provides
fun provideOkHttpClient(chuckerInterceptor: ChuckerInterceptor): OkHttpClient =
OkHttpClient.Builder()
.addNetworkInterceptor(chuckerInterceptor)
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
@OptIn(ExperimentalSerializationApi::class)
@Singleton
@Provides
fun provideRetrofit(
okHttpClient: OkHttpClient,
json: Json,
appInfo: AppInfo
): Retrofit = Retrofit.Builder()
.baseUrl(appInfo.messagesBaseUrl)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()
@Singleton
@Provides
fun provideAdminMessageService(retrofit: Retrofit): AdminMessageService = retrofit.create()
@Singleton
@Provides
@ -67,14 +105,6 @@ internal class RepositoryModule {
appInfo: AppInfo
) = AppDatabase.newInstance(context, sharedPrefProvider, appInfo)
@Singleton
@Provides
fun provideResources(@ApplicationContext context: Context): Resources = context.resources
@Singleton
@Provides
fun provideAssets(@ApplicationContext context: Context): AssetManager = context.assets
@Singleton
@Provides
fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences =
@ -208,4 +238,8 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideNotificationDao(database: AppDatabase) = database.notificationDao
@Singleton
@Provides
fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao
}

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.data.api
import io.github.wulkanowy.data.db.entities.AdminMessage
import retrofit2.http.GET
import javax.inject.Singleton
@Singleton
interface AdminMessageService {
@GET("/v1.json")
suspend fun getAdminMessages(): List<AdminMessage>
}

View File

@ -6,6 +6,7 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.RoomDatabase.JournalMode.TRUNCATE
import androidx.room.TypeConverters
import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
@ -35,6 +36,7 @@ import io.github.wulkanowy.data.db.dao.TeacherDao
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.dao.TimetableHeaderDao
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.CompletedLesson
@ -97,8 +99,9 @@ import io.github.wulkanowy.data.db.migrations.Migration37
import io.github.wulkanowy.data.db.migrations.Migration38
import io.github.wulkanowy.data.db.migrations.Migration39
import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration41
import io.github.wulkanowy.data.db.migrations.Migration40
import io.github.wulkanowy.data.db.migrations.Migration41
import io.github.wulkanowy.data.db.migrations.Migration42
import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration7
@ -138,7 +141,8 @@ import javax.inject.Singleton
StudentInfo::class,
TimetableHeader::class,
SchoolAnnouncement::class,
Notification::class
Notification::class,
AdminMessage::class
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -147,7 +151,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 41
const val VERSION_SCHEMA = 42
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -189,7 +193,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration38(),
Migration39(),
Migration40(),
Migration41()
Migration41(),
Migration42()
)
fun newInstance(
@ -261,4 +266,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val schoolAnnouncementDao: SchoolAnnouncementDao
abstract val notificationDao: NotificationDao
abstract val adminMessagesDao: AdminMessageDao
}

View File

@ -0,0 +1,25 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import io.github.wulkanowy.data.db.entities.AdminMessage
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@Singleton
@Dao
abstract class AdminMessageDao : BaseDao<AdminMessage> {
@Query("SELECT * FROM AdminMessages")
abstract fun loadAll(): Flow<List<AdminMessage>>
@Transaction
open suspend fun removeOldAndSaveNew(
oldMessages: List<AdminMessage>,
newMessages: List<AdminMessage>
) {
deleteAll(oldMessages)
insertAll(newMessages)
}
}

View File

@ -0,0 +1,37 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Serializable
@Entity(tableName = "AdminMessages")
data class AdminMessage(
@PrimaryKey
val id: Int,
val title: String,
val content: String,
@ColumnInfo(name = "version_name")
val versionMin: Int? = null,
@ColumnInfo(name = "version_max")
val versionMax: Int? = null,
@ColumnInfo(name = "target_register_host")
val targetRegisterHost: String? = null,
@ColumnInfo(name = "target_flavor")
val targetFlavor: String? = null,
@ColumnInfo(name = "destination_url")
val destinationUrl: String? = null,
val priority: String,
val type: String
)

View File

@ -0,0 +1,24 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration42 : Migration(41, 42) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""CREATE TABLE IF NOT EXISTS `AdminMessages` (
`id` INTEGER NOT NULL,
`title` TEXT NOT NULL,
`content` TEXT NOT NULL,
`version_name` INTEGER,
`version_max` INTEGER,
`target_register_host` TEXT,
`target_flavor` TEXT,
`destination_url` TEXT,
`priority` TEXT NOT NULL,
`type` TEXT NOT NULL,
PRIMARY KEY(`id`))"""
)
}
}

View File

@ -0,0 +1,53 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AdminMessageRepository @Inject constructor(
private val adminMessageService: AdminMessageService,
private val adminMessageDao: AdminMessageDao,
private val appInfo: AppInfo,
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "admin_messages"
suspend fun getAdminMessages(student: Student, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
query = { adminMessageDao.loadAll() },
fetch = { adminMessageService.getAdminMessages() },
shouldFetch = {
refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) || forceRefresh
},
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
refreshHelper.updateLastRefreshTimestamp(cacheKey)
},
showSavedOnLoading = false,
mapResult = { adminMessages ->
adminMessages.filter { adminMessage ->
val isCorrectRegister = adminMessage.targetRegisterHost?.let {
student.scrapperBaseUrl.contains(it, true)
} ?: true
val isCorrectFlavor =
adminMessage.targetFlavor?.equals(appInfo.buildFlavor, true) ?: true
val isCorrectMaxVersion =
adminMessage.versionMax?.let { it >= appInfo.versionCode } ?: true
val isCorrectMinVersion =
adminMessage.versionMin?.let { it <= appInfo.versionCode } ?: true
isCorrectRegister && isCorrectFlavor && isCorrectMaxVersion && isCorrectMinVersion
}.maxByOrNull { it.id }
}
)
}

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.repositories
import android.content.res.AssetManager
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.pojos.Contributor
import io.github.wulkanowy.utils.DispatchersProvider
import kotlinx.coroutines.withContext
@ -12,7 +13,7 @@ import javax.inject.Singleton
@Singleton
class AppCreatorRepository @Inject constructor(
private val assets: AssetManager,
@ApplicationContext private val context: Context,
private val dispatchers: DispatchersProvider,
private val json: Json,
) {
@ -20,7 +21,7 @@ class AppCreatorRepository @Inject constructor(
@OptIn(ExperimentalSerializationApi::class)
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) {
val inputStream = assets.open("contributors.json").buffered()
val inputStream = context.assets.open("contributors.json").buffered()
json.decodeFromStream<List<Contributor>>(inputStream)
}
}

View File

@ -209,6 +209,7 @@ class PreferencesRepository @Inject constructor(
.map { set ->
set.map { DashboardItem.Tile.valueOf(it) }
.plus(DashboardItem.Tile.ACCOUNT)
.plus(DashboardItem.Tile.ADMIN_MESSAGE)
.toSet()
}
@ -216,6 +217,7 @@ class PreferencesRepository @Inject constructor(
get() = selectedDashboardTilesPreference.get()
.map { DashboardItem.Tile.valueOf(it) }
.plus(DashboardItem.Tile.ACCOUNT)
.plus(DashboardItem.Tile.ADMIN_MESSAGE)
.toSet()
set(value) {
val filteredValue = value.filterNot { it == DashboardItem.Tile.ACCOUNT }

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.base
import android.content.res.Resources
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
@ -9,7 +10,7 @@ import io.github.wulkanowy.utils.security.ScramblerException
import timber.log.Timber
import javax.inject.Inject
open class ErrorHandler @Inject constructor(protected val resources: Resources) {
open class ErrorHandler @Inject constructor(@ApplicationContext protected val context: Context) {
var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> }
@ -25,7 +26,7 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources)
}
protected open fun proceed(error: Throwable) {
showErrorMessage(resources.getString(error), error)
showErrorMessage(context.resources.getString(error), error)
when (error) {
is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl)
is ScramblerException, is BadCredentialsException -> onSessionExpired()

View File

@ -1,6 +1,8 @@
package io.github.wulkanowy.ui.modules.dashboard
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.Typeface
import android.os.Handler
import android.os.Looper
@ -18,6 +20,7 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.databinding.ItemDashboardAccountBinding
import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding
import io.github.wulkanowy.databinding.ItemDashboardAnnouncementsBinding
import io.github.wulkanowy.databinding.ItemDashboardConferencesBinding
import io.github.wulkanowy.databinding.ItemDashboardExamsBinding
@ -63,6 +66,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
var onConferencesTileClickListener: () -> Unit = {}
var onAdminMessageClickListener: (String?) -> Unit = {}
val items = mutableListOf<DashboardItem>()
fun submitList(newItems: List<DashboardItem>) {
@ -109,6 +114,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
DashboardItem.Type.CONFERENCES.ordinal -> ConferencesViewHolder(
ItemDashboardConferencesBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false)
)
else -> throw IllegalArgumentException()
}
}
@ -123,6 +131,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position)
is ExamsViewHolder -> bindExamsViewHolder(holder, position)
is ConferencesViewHolder -> bindConferencesViewHolder(holder, position)
is AdminMessageViewHolder -> bindAdminMessage(holder, position)
}
}
@ -697,6 +706,34 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
}
}
private fun bindAdminMessage(adminMessageViewHolder: AdminMessageViewHolder, position: Int) {
val item = (items[position] as DashboardItem.AdminMessages).adminMessage ?: return
val context = adminMessageViewHolder.binding.root.context
val (backgroundColor, textColor) = when (item.priority) {
"HIGH" -> {
context.getThemeAttrColor(R.attr.colorPrimary) to
context.getThemeAttrColor(R.attr.colorOnPrimary)
}
"MEDIUM" -> {
context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK
}
else -> null to context.getThemeAttrColor(R.attr.colorOnSurface)
}
with(adminMessageViewHolder.binding) {
dashboardAdminMessageItemTitle.text = item.title
dashboardAdminMessageItemTitle.setTextColor(textColor)
dashboardAdminMessageItemDescription.text = item.content
dashboardAdminMessageItemDescription.setTextColor(textColor)
dashboardAdminMessageItemIcon.setColorFilter(textColor)
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
root.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}
class AccountViewHolder(val binding: ItemDashboardAccountBinding) :
RecyclerView.ViewHolder(binding.root)
@ -736,6 +773,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val adapter by lazy { DashboardConferencesAdapter() }
}
class AdminMessageViewHolder(val binding: ItemDashboardAdminMessageBinding) :
RecyclerView.ViewHolder(binding.root)
private class DiffCallback(
private val newList: List<DashboardItem>,
private val oldList: List<DashboardItem>

View File

@ -10,6 +10,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentDashboardBinding
@ -29,6 +30,7 @@ import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragm
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate
import javax.inject.Inject
@ -97,6 +99,13 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
onConferencesTileClickListener = {
mainActivity.pushView(ConferenceFragment.newInstance())
}
onAdminMessageClickListener = presenter::onAdminMessageSelected
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
binding.dashboardRecycler.scrollToPosition(0)
}
})
}
with(binding) {
@ -188,6 +197,10 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
(requireActivity() as MainActivity).pushView(NotificationsCenterFragment.newInstance())
}
override fun openInternetBrowser(url: String) {
requireContext().openInternetBrowser(url)
}
override fun onDestroyView() {
dashboardAdapter.clearTimers()
presenter.onDetachView()

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.dashboard
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Grade
@ -16,6 +17,15 @@ sealed class DashboardItem(val type: Type) {
abstract val isDataLoaded: Boolean
data class AdminMessages(
val adminMessage: AdminMessage? = null,
override val error: Throwable? = null,
override val isLoading: Boolean = false
) : DashboardItem(Type.ADMIN_MESSAGE) {
override val isDataLoaded get() = adminMessage != null
}
data class Account(
val student: Student? = null,
override val error: Throwable? = null,
@ -96,6 +106,7 @@ sealed class DashboardItem(val type: Type) {
}
enum class Type {
ADMIN_MESSAGE,
ACCOUNT,
HORIZONTAL_GROUP,
LESSONS,
@ -108,6 +119,7 @@ sealed class DashboardItem(val type: Type) {
}
enum class Tile {
ADMIN_MESSAGE,
ACCOUNT,
LUCKY_NUMBER,
MESSAGES,
@ -123,6 +135,7 @@ sealed class DashboardItem(val type: Type) {
}
fun DashboardItem.Tile.toDashboardItemType() = when (this) {
DashboardItem.Tile.ADMIN_MESSAGE -> DashboardItem.Type.ADMIN_MESSAGE
DashboardItem.Tile.ACCOUNT -> DashboardItem.Type.ACCOUNT
DashboardItem.Tile.LUCKY_NUMBER -> DashboardItem.Type.HORIZONTAL_GROUP
DashboardItem.Tile.MESSAGES -> DashboardItem.Type.HORIZONTAL_GROUP

View File

@ -21,7 +21,7 @@ class DashboardItemMoveCallback(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = if (viewHolder.bindingAdapterPosition != 0) {
val dragFlags = if (!viewHolder.isAdminMessageOrAccountItem) {
ItemTouchHelper.UP or ItemTouchHelper.DOWN
} else 0
@ -32,7 +32,7 @@ class DashboardItemMoveCallback(
recyclerView: RecyclerView,
current: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
) = target.bindingAdapterPosition != 0
) = !target.isAdminMessageOrAccountItem
override fun onMove(
recyclerView: RecyclerView,
@ -52,4 +52,7 @@ class DashboardItemMoveCallback(
onUserInteractionEndListener(dashboardAdapter.items.toList())
}
private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean
get() = this is DashboardAdapter.AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder
}

View File

@ -5,6 +5,7 @@ import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.AdminMessageRepository
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.ConferenceRepository
import io.github.wulkanowy.data.repositories.ExamRepository
@ -50,7 +51,8 @@ class DashboardPresenter @Inject constructor(
private val examRepository: ExamRepository,
private val conferenceRepository: ConferenceRepository,
private val preferencesRepository: PreferencesRepository,
private val schoolAnnouncementRepository: SchoolAnnouncementRepository
private val schoolAnnouncementRepository: SchoolAnnouncementRepository,
private val adminMessageRepository: AdminMessageRepository
) : BasePresenter<DashboardView>(errorHandler, studentRepository) {
private val dashboardItemLoadedList = mutableListOf<DashboardItem>()
@ -179,6 +181,7 @@ class DashboardPresenter @Inject constructor(
loadConferences(student, forceRefresh)
}
DashboardItem.Type.ADS -> TODO()
DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh)
}
}
}
@ -225,6 +228,10 @@ class DashboardPresenter @Inject constructor(
}.toSet()
}
fun onAdminMessageSelected(url: String?) {
url?.let { view?.openInternetBrowser(it) }
}
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow {
val semester = semesterRepository.getCurrentSemester(student)
@ -567,6 +574,38 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_conferences")
}
private fun loadAdminMessage(student: Student, forceRefresh: Boolean) {
flowWithResourceIn { adminMessageRepository.getAdminMessages(student, forceRefresh) }
.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard admin message data started")
if (forceRefresh) return@onEach
updateData(DashboardItem.AdminMessages(), forceRefresh)
}
Status.SUCCESS -> {
Timber.i("Loading dashboard admin message result: Success")
updateData(
dashboardItem = DashboardItem.AdminMessages(adminMessage = it.data),
forceRefresh = forceRefresh
)
}
Status.ERROR -> {
Timber.i("Loading dashboard admin message result: An exception occurred")
errorHandler.dispatch(it.error!!)
updateData(
dashboardItem = DashboardItem.AdminMessages(
adminMessage = it.data,
error = it.error
),
forceRefresh = forceRefresh
)
}
}
}
.launch("dashboard_admin_messages")
}
private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) {
val isForceRefreshError = forceRefresh && dashboardItem.error != null
val isFirstRunDataLoadedError =
@ -579,6 +618,11 @@ class DashboardPresenter @Inject constructor(
sortDashboardItems()
if (dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded) {
dashboardItemsToLoad = dashboardItemsToLoad - DashboardItem.Type.ADMIN_MESSAGE
dashboardItemLoadedList.removeAll { it.type == DashboardItem.Type.ADMIN_MESSAGE }
}
if (forceRefresh) {
updateForceRefreshData(dashboardItem)
} else {
@ -644,7 +688,9 @@ class DashboardPresenter @Inject constructor(
itemsLoadedList: List<DashboardItem>,
forceRefresh: Boolean
) {
val filteredItems = itemsLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT }
val filteredItems = itemsLoadedList.filterNot {
it.type == DashboardItem.Type.ACCOUNT || it.type == DashboardItem.Type.ADMIN_MESSAGE
}
val isAccountItemError =
itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null
val isGeneralError =
@ -676,10 +722,13 @@ class DashboardPresenter @Inject constructor(
val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition
dashboardItemLoadedList.sortBy { tile ->
dashboardItemsPosition?.getOrDefault(
tile.type,
val defaultPosition = if (tile is DashboardItem.AdminMessages) {
-1
} else {
tile.type.ordinal + 100
) ?: tile.type.ordinal
}
dashboardItemsPosition?.getOrDefault(tile.type, defaultPosition) ?: tile.type.ordinal
}
}
}

View File

@ -25,4 +25,6 @@ interface DashboardView : BaseView {
fun popViewToRoot()
fun openNotificationsCenterView()
fun openInternetBrowser(url: String)
}

View File

@ -1,7 +1,8 @@
package io.github.wulkanowy.ui.modules.login
import android.content.res.Resources
import android.content.Context
import android.database.sqlite.SQLiteConstraintException
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.sdk.mobile.exception.InvalidPinException
import io.github.wulkanowy.sdk.mobile.exception.InvalidSymbolException
@ -11,7 +12,8 @@ import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.ui.base.ErrorHandler
import javax.inject.Inject
class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) {
class LoginErrorHandler @Inject constructor(@ApplicationContext context: Context) :
ErrorHandler(context) {
var onBadCredentials: (String?) -> Unit = {}
@ -24,6 +26,7 @@ class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler
var onStudentDuplicate: (String) -> Unit = {}
override fun proceed(error: Throwable) {
val resources = context.resources
when (error) {
is BadCredentialsException -> onBadCredentials(error.message)
is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student))

View File

@ -1,13 +1,15 @@
package io.github.wulkanowy.ui.modules.login.recover
import android.content.res.Resources
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.sdk.scrapper.exception.InvalidCaptchaException
import io.github.wulkanowy.sdk.scrapper.exception.InvalidEmailException
import io.github.wulkanowy.sdk.scrapper.exception.NoAccountFoundException
import io.github.wulkanowy.ui.base.ErrorHandler
import javax.inject.Inject
class RecoverErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) {
class RecoverErrorHandler @Inject constructor(@ApplicationContext context: Context) :
ErrorHandler(context) {
var onInvalidUsername: (String) -> Unit = {}
@ -15,7 +17,8 @@ class RecoverErrorHandler @Inject constructor(resources: Resources) : ErrorHandl
override fun proceed(error: Throwable) {
when (error) {
is InvalidEmailException, is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty())
is InvalidEmailException,
is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty())
is InvalidCaptchaException -> onInvalidCaptcha(error.localizedMessage.orEmpty(), error)
else -> super.proceed(error)
}

View File

@ -1,11 +1,13 @@
package io.github.wulkanowy.ui.modules.timetable.completed
import android.content.res.Resources
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.ui.base.ErrorHandler
import javax.inject.Inject
class CompletedLessonsErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) {
class CompletedLessonsErrorHandler @Inject constructor(@ApplicationContext context: Context) :
ErrorHandler(context) {
var onFeatureDisabled: () -> Unit = {}

View File

@ -1,35 +1,31 @@
package io.github.wulkanowy.utils
import android.content.res.Resources
import android.os.Build.MANUFACTURER
import android.os.Build.MODEL
import android.os.Build.VERSION.SDK_INT
import io.github.wulkanowy.BuildConfig.BUILD_TIMESTAMP
import io.github.wulkanowy.BuildConfig.DEBUG
import io.github.wulkanowy.BuildConfig.FLAVOR
import io.github.wulkanowy.BuildConfig.VERSION_CODE
import io.github.wulkanowy.BuildConfig.VERSION_NAME
import android.os.Build
import io.github.wulkanowy.BuildConfig
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
open class AppInfo @Inject constructor() {
open val isDebug get() = DEBUG
open val isDebug get() = BuildConfig.DEBUG
open val versionCode get() = VERSION_CODE
open val versionCode get() = BuildConfig.VERSION_CODE
open val buildTimestamp get() = BUILD_TIMESTAMP
open val buildTimestamp get() = BuildConfig.BUILD_TIMESTAMP
open val buildFlavor get() = FLAVOR
open val buildFlavor get() = BuildConfig.FLAVOR
open val versionName get() = VERSION_NAME
open val versionName get() = BuildConfig.VERSION_NAME
open val systemVersion get() = SDK_INT
open val systemVersion get() = Build.VERSION.SDK_INT
open val systemManufacturer: String get() = MANUFACTURER
open val systemManufacturer: String get() = Build.MANUFACTURER
open val systemModel: String get() = MODEL
open val systemModel: String get() = Build.MODEL
open val messagesBaseUrl = BuildConfig.MESSAGES_BASE_URL
@Suppress("DEPRECATION")
open val systemLanguage: String

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,5.99L19.53,19L4.47,19L12,5.99M2.74,18c-0.77,1.33 0.19,3 1.73,3h15.06c1.54,0 2.5,-1.67 1.73,-3L13.73,4.99c-0.77,-1.33 -2.69,-1.33 -3.46,0L2.74,18zM11,11v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1zM11,16h2v2h-2z" />
</vector>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="6dp"
app:cardElevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/dashboard_admin_message_item_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/dashboard_admin_message_item_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_error"
app:layout_constraintBottom_toBottomOf="@id/dashboard_admin_message_item_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/dashboard_admin_message_item_title"
tools:ignore="ContentDescription"
tools:tint="@android:color/black" />
<TextView
android:id="@+id/dashboard_admin_message_item_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textStyle="bold"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/dashboard_admin_message_item_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/dashboard_admin_message_item_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dashboard_admin_message_item_title"
app:lineHeight="20dp"
tools:maxLines="5"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -8,6 +8,7 @@
<item name="colorError">@color/colorErrorLight</item>
<item name="colorDivider">@color/colorDividerInverse</item>
<item name="colorSwipeRefresh">@color/colorSwipeRefreshDark</item>
<item name="colorMessageMedium">@color/dashboard_message_medium_light</item>
<item name="android:windowBackground">?colorSurface</item>
<item name="android:textColor">?android:textColorPrimary</item>
<item name="android:navigationBarColor">@color/colorNavigationBarLight</item>

View File

@ -40,7 +40,7 @@
<item>https://vulcan.net.pl/?login</item>
<item>https://vulcan.net.pl/?login</item>
<item>https://vulcan.net.pl/?login</item>
<item>http://fakelog.tk/?email</item>
<item>http://fakelog.cf/?email</item>
</string-array>
<string-array name="hosts_symbols">
<item>Default</item>

View File

@ -3,4 +3,5 @@
<attr name="colorDivider" format="color" />
<attr name="colorSwipeRefresh" format="color" />
<attr name="colorTimetableChange" format="color" />
<attr name="colorMessageMedium" format="color" />
</resources>

View File

@ -12,6 +12,9 @@
<color name="colorStatusBarLight">#1C1C1C</color>
<color name="colorStatusBarBlack">#0D0D0D</color>
<color name="dashboard_message_medium_light">#FFD980</color>
<color name="dashboard_message_medium_dark">#ffd54f</color>
<color name="timetable_change_light">#ffd54f</color>
<color name="timetable_change_dark">#ff8f00</color>

View File

@ -11,6 +11,7 @@
<item name="colorError">@color/colorError</item>
<item name="colorDivider">@color/colorDivider</item>
<item name="colorSwipeRefresh">@color/colorSwipeRefresh</item>
<item name="colorMessageMedium">@color/dashboard_message_medium_dark</item>
<item name="android:statusBarColor">@android:color/darker_gray</item>
<item name="android:textColor">?android:textColorPrimary</item>
<item name="android:preferenceStyle">@style/PreferenceThemeOverlay</item>