1
0
Fork 1

Compare commits

..

No commits in common. "1.4.0" and "1.2.1" have entirely different histories.
1.4.0 ... 1.2.1

331 changed files with 2432 additions and 15916 deletions

View file

@ -28,14 +28,15 @@ jobs:
SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/google-services.json.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
- name: Upload apk to google play
env:
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
ANDROID_PUBLISHER_CREDENTIALS: ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }}
run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace;
PLAY_SERVICE_ACCOUNT_EMAIL: ${{ secrets.PLAY_SERVICE_ACCOUNT_EMAIL }}
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
run: ./gradlew publishPlayRelease -PenableFirebase --stacktrace;
deploy-app-gallery:
name: Deploy to AppGallery
@ -59,6 +60,7 @@ jobs:
SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
- name: Prepare credentials
env:
@ -66,7 +68,7 @@ jobs:
run: echo $AGC_CREDENTIALS > ./app/src/release/agconnect-credentials.json
- name: Build and publish HMS version
env:
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
run: ./gradlew bundleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
run: ./gradlew assembleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace

View file

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021 Wulkanowy
Copyright 2019 Wulkanowy
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -1,6 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
@ -15,22 +14,23 @@ apply from: 'sonarqube.gradle'
apply from: 'hooks.gradle'
android {
compileSdkVersion 31
compileSdkVersion 30
defaultConfig {
applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 31
versionCode 98
versionName "1.4.0"
targetSdkVersion 30
versionCode 94
versionName "1.2.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
resValue "string", "app_name", "Wulkanowy"
buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis())
manifestPlaceholders = [
firebase_enabled: project.hasProperty("enableFirebase"),
admob_project_id: ""
firebase_enabled: project.hasProperty("enableFirebase")
]
javaCompileOptions {
annotationProcessorOptions {
@ -40,14 +40,6 @@ android {
]
}
}
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
if (System.env.SET_BUILD_TIMESTAMP) {
buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis())
} else {
buildConfigField "long", "BUILD_TIMESTAMP", "1486235849000"
}
}
sourceSets {
@ -70,14 +62,12 @@ 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"
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\""
}
}
@ -86,38 +76,30 @@ android {
productFlavors {
hms {
dimension "platform"
manifestPlaceholders = [install_channel: "AppGallery"]
manifestPlaceholders = [
install_channel: "AppGallery"
]
}
play {
dimension "platform"
manifestPlaceholders = [
install_channel : "Google Play",
admob_project_id: System.getenv("ADMOB_PROJECT_ID") ?: "ca-app-pub-3940256099942544~3347511713"
install_channel: "Google Play"
]
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "\"${System.getenv("SINGLE_SUPPORT_AD_ID") ?: "ca-app-pub-3940256099942544/5354046379"}\""
}
fdroid {
dimension "platform"
manifestPlaceholders = [install_channel: "F-Droid"]
manifestPlaceholders = [
install_channel: "F-Droid"
]
}
}
playConfigs {
play { enabled.set(true) }
}
buildFeatures {
viewBinding true
}
bundle {
language {
enableSplit = false
}
}
testOptions.unitTests {
includeAndroidResources = true
}
@ -148,61 +130,61 @@ kapt {
}
play {
serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf"
serviceAccountCredentials = file('key.p12')
defaultToAppBundles = false
track = 'beta'
updatePriority = 1
enabled.set(false)
track = 'production'
updatePriority = 3
}
huaweiPublish {
instances {
hmsRelease {
credentialsPath = "$rootDir/app/src/release/agconnect-credentials.json"
buildFormat = "aab"
buildFormat = "apk"
deployType = "draft"
}
}
}
ext {
work_manager = "2.7.0"
work_manager = "2.5.0"
android_hilt = "1.0.0"
room = "2.3.0"
chucker = "3.5.2"
mockk = "1.12.0"
coroutines = "1.5.2"
moshi = "1.12.0"
}
dependencies {
implementation "io.github.wulkanowy:sdk:1.4.0"
implementation "io.github.wulkanowy:sdk:1.2.1"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
implementation "androidx.core:core-ktx:1.7.0"
implementation 'androidx.core:core-splashscreen:1.0.0-alpha02'
implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.appcompat:appcompat:1.4.0-rc01"
implementation "androidx.fragment:fragment-ktx:1.4.0-rc01"
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core-ktx:1.6.0"
implementation "androidx.activity:activity-ktx:1.3.1"
implementation "androidx.appcompat:appcompat:1.3.1"
implementation "androidx.appcompat:appcompat-resources:1.3.1"
implementation "androidx.fragment:fragment-ktx:1.3.6"
implementation "androidx.annotation:annotation:1.2.0"
implementation "androidx.preference:preference-ktx:1.1.1"
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.1"
implementation "androidx.constraintlayout:constraintlayout:2.1.0"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.4.0"
implementation "com.github.wulkanowy:material-chips-input:2.3.1"
implementation "com.github.wulkanowy:material-chips-input:2.2.0"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.github.lopspower:CircularImageView:4.2.0'
implementation "androidx.work:work-runtime-ktx:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-ktx:$room"
@ -216,41 +198,40 @@ 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.squareup.moshi:moshi:$moshi"
implementation "com.squareup.moshi:moshi-adapters:$moshi"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi"
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'
implementation 'com.github.bastienpaulfr:Treessence:1.0.4'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation "io.coil-kt:coil:1.4.0"
implementation "io.coil-kt:coil:1.3.2"
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
implementation 'com.fredporciuncula:flow-preferences:1.5.0'
playImplementation platform('com.google.firebase:firebase-bom:29.0.0')
playImplementation platform('com.google.firebase:firebase-bom:28.4.0')
playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.android.play:core:1.10.2'
playImplementation 'com.google.android.play:core:1.10.1'
playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.android.gms:play-services-ads:20.4.0'
hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.303'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.300'
hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.0.300'
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:1.0.6'
debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:v1.0.6'
testImplementation "junit:junit:4.13.2"
testImplementation "io.mockk:mockk:$mockk"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2'
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.7'
testImplementation 'org.robolectric:robolectric:4.6.1'
testImplementation "androidx.test:runner:1.4.0"
testImplementation "androidx.test.ext:junit:1.1.3"
testImplementation "androidx.test:core:1.4.0"

BIN
app/key.p12.gpg Normal file

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -38,14 +38,13 @@
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false"
android:theme="@style/WulkanowyTheme"
android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<activity
android:name=".ui.modules.splash.SplashActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/WulkanowyTheme.SplashScreen"
tools:ignore="LockedOrientationActivity">
@ -75,7 +74,6 @@
<activity
android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity"
android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter>
@ -85,7 +83,6 @@
<activity
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity"
android:excludeFromRecents="true"
android:exported="true"
android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter>
@ -96,23 +93,6 @@
<service
android:name=".services.widgets.TimetableWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name=".services.piggyback.VulcanNotificationListenerService"
android:exported="true"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<service
android:name=".services.messaging.AppMessagingService"
android:exported="false"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<receiver
android:name=".ui.modules.timetablewidget.TimetableWidgetProvider"
@ -127,7 +107,6 @@
</receiver>
<receiver
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetProvider"
android:exported="true"
android:label="@string/lucky_number_title">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@ -140,9 +119,11 @@
<receiver android:name=".services.alarm.TimetableNotificationReceiver" />
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
android:exported="false"
tools:node="remove" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
@ -153,44 +134,44 @@
android:resource="@xml/provider_paths" />
</provider>
<meta-data
android:name="install_channel"
android:value="${install_channel}" />
<!-- workaround for https://github.com/firebase/firebase-android-sdk/issues/473 enabled:false -->
<!-- https://firebase.googleblog.com/2017/03/take-control-of-your-firebase-init-on.html -->
<provider
android:name="com.google.firebase.provider.FirebaseInitProvider"
android:authorities="${applicationId}.firebaseinitprovider"
android:enabled="${firebase_enabled}"
android:exported="false"
tools:ignore="MissingClass" />
android:exported="false" />
<meta-data
android:name="install_channel"
android:value="${install_channel}" />
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="google_analytics_adid_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="firebase_messaging_auto_init_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="firebase_inapp_messaging_auto_data_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_all" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="push_channel" />
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="${admob_project_id}" />
<meta-data
android:name="com.google.android.gms.ads.DELAY_APP_MEASUREMENT_INIT"
android:value="true" />
</application>
</manifest>

View file

@ -1,10 +1,12 @@
package io.github.wulkanowy
import android.annotation.SuppressLint
import android.app.Application
import android.util.Log.DEBUG
import android.util.Log.INFO
import android.util.Log.VERBOSE
import android.webkit.WebView
import androidx.fragment.app.FragmentManager
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.yariksoffice.lingver.Lingver
@ -39,8 +41,10 @@ class WulkanowyApp : Application(), Configuration.Provider {
@Inject
lateinit var analyticsHelper: AnalyticsHelper
@SuppressLint("UnsafeOptInUsageWarning")
override fun onCreate() {
super.onCreate()
FragmentManager.enableNewStateManager(false)
initializeAppLanguage()
themeManager.applyDefaultTheme()
initLogging()

View file

@ -2,49 +2,48 @@ 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.squareup.moshi.Moshi
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 DataModule {
internal class RepositoryModule {
@Singleton
@Provides
fun provideSdk(chuckerInterceptor: ChuckerInterceptor) =
Sdk().apply {
fun provideSdk(chuckerCollector: ChuckerCollector, @ApplicationContext context: Context): Sdk {
return Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL
setSimpleHttpLogger { Timber.d(it) }
// for debug only
addInterceptor(chuckerInterceptor, network = true)
addInterceptor(
ChuckerInterceptor.Builder(context)
.collector(chuckerCollector)
.alwaysReadResponseBody(true)
.build(), network = true
)
}
}
@Singleton
@ -52,50 +51,13 @@ internal class DataModule {
fun provideChuckerCollector(
@ApplicationContext context: Context,
prefRepository: PreferencesRepository
) = ChuckerCollector(
): ChuckerCollector {
return 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
@ -105,6 +67,14 @@ internal class DataModule {
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 =
@ -118,9 +88,7 @@ internal class DataModule {
@Singleton
@Provides
fun provideJson() = Json {
ignoreUnknownKeys = true
}
fun provideMoshi() = Moshi.Builder().build()
@Singleton
@Provides
@ -234,12 +202,4 @@ internal class DataModule {
@Singleton
@Provides
fun provideSchoolAnnouncementDao(database: AppDatabase) = database.schoolAnnouncementDao
@Singleton
@Provides
fun provideNotificationDao(database: AppDatabase) = database.notificationDao
@Singleton
@Provides
fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao
}

View file

@ -1,12 +0,0 @@
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,11 +6,11 @@ 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
import io.github.wulkanowy.data.db.dao.ConferenceDao
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.dao.ExamDao
import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao
@ -23,10 +23,8 @@ import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.dao.NotificationDao
import io.github.wulkanowy.data.db.dao.RecipientDao
import io.github.wulkanowy.data.db.dao.ReportingUnitDao
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.dao.SchoolDao
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
@ -36,11 +34,11 @@ 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
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradePartialStatistics
@ -53,11 +51,9 @@ import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.School
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentInfo
@ -99,10 +95,6 @@ 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.Migration40
import io.github.wulkanowy.data.db.migrations.Migration41
import io.github.wulkanowy.data.db.migrations.Migration42
import io.github.wulkanowy.data.db.migrations.Migration43
import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration7
@ -142,8 +134,6 @@ import javax.inject.Singleton
StudentInfo::class,
TimetableHeader::class,
SchoolAnnouncement::class,
Notification::class,
AdminMessage::class
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -152,7 +142,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 43
const val VERSION_SCHEMA = 39
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -193,10 +183,6 @@ abstract class AppDatabase : RoomDatabase() {
Migration37(),
Migration38(),
Migration39(),
Migration40(),
Migration41(sharedPrefProvider),
Migration42(),
Migration43()
)
fun newInstance(
@ -266,8 +252,4 @@ abstract class AppDatabase : RoomDatabase() {
abstract val timetableHeaderDao: TimetableHeaderDao
abstract val schoolAnnouncementDao: SchoolAnnouncementDao
abstract val notificationDao: NotificationDao
abstract val adminMessagesDao: AdminMessageDao
}

View file

@ -1,10 +1,9 @@
package io.github.wulkanowy.data.db
import androidx.room.TypeConverter
import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import io.github.wulkanowy.data.db.adapters.PairAdapterFactory
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
@ -14,7 +13,15 @@ import java.util.Date
class Converters {
private val json = Json
private val moshi by lazy { Moshi.Builder().add(PairAdapterFactory).build() }
private val integerListAdapter by lazy {
moshi.adapter<List<Int>>(Types.newParameterizedType(List::class.java, Integer::class.java))
}
private val stringListPairAdapter by lazy {
moshi.adapter<List<Pair<String, String>>>(Types.newParameterizedType(List::class.java, Pair::class.java, String::class.java, String::class.java))
}
@TypeConverter
fun timestampToDate(value: Long?): LocalDate? = value?.run {
@ -44,25 +51,21 @@ class Converters {
@TypeConverter
fun intListToJson(list: List<Int>): String {
return json.encodeToString(list)
return integerListAdapter.toJson(list)
}
@TypeConverter
fun jsonToIntList(value: String): List<Int> {
return json.decodeFromString(value)
return integerListAdapter.fromJson(value).orEmpty()
}
@TypeConverter
fun stringPairListToJson(list: List<Pair<String, String>>): String {
return json.encodeToString(list)
return stringListPairAdapter.toJson(list)
}
@TypeConverter
fun jsonToStringPairList(value: String): List<Pair<String, String>> {
return try {
json.decodeFromString(value)
} catch (e: SerializationException) {
emptyList() // handle errors from old gson Pair serialized data
}
return stringListPairAdapter.fromJson(value).orEmpty()
}
}

View file

@ -22,14 +22,11 @@ class SharedPrefProvider @Inject constructor(
fun getString(key: String) = sharedPref.getString(key, null)
fun getString(key: String, defaultValue: String): String =
sharedPref.getString(key, defaultValue) ?: defaultValue
fun getString(key: String, defaultValue: String): String = sharedPref.getString(key, defaultValue) ?: defaultValue
fun getBoolean(key: String, defaultValue: Boolean): Boolean =
sharedPref.getBoolean(key, defaultValue)
fun getBoolean(key: String, defaultValue: Boolean): Boolean = sharedPref.getBoolean(key, defaultValue)
fun putBoolean(key: String, value: Boolean, sync: Boolean = false) =
sharedPref.edit(sync) { putBoolean(key, value) }
fun putBoolean(key: String, value: Boolean, sync: Boolean = false) = sharedPref.edit(sync) { putBoolean(key, value) }
fun putString(key: String, value: String?, sync: Boolean = false) {
sharedPref.edit(sync) { putString(key, value) }

View file

@ -0,0 +1,68 @@
package io.github.wulkanowy.data.db.adapters
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
object PairAdapterFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
if (type !is ParameterizedType || List::class.java != type.rawType) return null
if (type.actualTypeArguments[0] != Pair::class.java) return null
val listType = Types.newParameterizedType(List::class.java, Map::class.java, String::class.java)
val listAdapter = moshi.adapter<List<Map<String, String>>>(listType)
val mapType = Types.newParameterizedType(MutableMap::class.java, String::class.java, String::class.java)
val mapAdapter = moshi.adapter<Map<String, String>>(mapType)
return PairAdapter(listAdapter, mapAdapter)
}
private class PairAdapter(
private val listAdapter: JsonAdapter<List<Map<String, String>>>,
private val mapAdapter: JsonAdapter<Map<String, String>>,
) : JsonAdapter<List<Pair<String, String>>>() {
override fun toJson(writer: JsonWriter, value: List<Pair<String, String>>?) {
writer.beginArray()
value?.forEach {
writer.beginObject()
writer.name("first").value(it.first)
writer.name("second").value(it.second)
writer.endObject()
}
writer.endArray()
}
override fun fromJson(reader: JsonReader): List<Pair<String, String>>? {
return if (reader.peek() == JsonReader.Token.BEGIN_OBJECT) deserializeMoshiMap(reader)
else deserializeGsonPair(reader)
}
// for compatibility with 0.21.0
private fun deserializeMoshiMap(reader: JsonReader): List<Pair<String, String>>? {
val map = mapAdapter.fromJson(reader) ?: return null
return map.entries.map {
it.key to it.value
}
}
private fun deserializeGsonPair(reader: JsonReader): List<Pair<String, String>>? {
val list = listAdapter.fromJson(reader) ?: return null
return list.map {
require(it.size == 2) {
"pair with more or less than two elements: $list"
}
it["first"].orEmpty() to it["second"].orEmpty()
}
}
}
}

View file

@ -1,25 +0,0 @@
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

@ -11,11 +11,6 @@ import javax.inject.Singleton
@Dao
interface AttendanceDao : BaseDao<Attendance> {
@Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :start AND date <= :end")
fun loadAll(
diaryId: Int,
studentId: Int,
start: LocalDate,
end: LocalDate
): Flow<List<Attendance>>
@Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow<List<Attendance>>
}

View file

@ -1,15 +0,0 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Notification
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@Singleton
@Dao
interface NotificationDao : BaseDao<Notification> {
@Query("SELECT * FROM Notifications WHERE student_id = :studentId OR student_id = -1")
fun loadAll(studentId: Long): Flow<List<Notification>>
}

View file

@ -14,39 +14,33 @@ import javax.inject.Singleton
@Singleton
@Dao
abstract class StudentDao {
interface StudentDao {
@Insert(onConflict = ABORT)
abstract suspend fun insertAll(student: List<Student>): List<Long>
suspend fun insertAll(student: List<Student>): List<Long>
@Delete
abstract suspend fun delete(student: Student)
suspend fun delete(student: Student)
@Update(entity = Student::class)
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@Query("SELECT * FROM Students WHERE is_current = 1")
abstract suspend fun loadCurrent(): Student?
suspend fun loadCurrent(): Student?
@Query("SELECT * FROM Students WHERE id = :id")
abstract suspend fun loadById(id: Long): Student?
suspend fun loadById(id: Long): Student?
@Query("SELECT * FROM Students")
abstract suspend fun loadAll(): List<Student>
suspend fun loadAll(): List<Student>
@Transaction
@Query("SELECT * FROM Students")
abstract suspend fun loadStudentsWithSemesters(): List<StudentWithSemesters>
suspend fun loadStudentsWithSemesters(): List<StudentWithSemesters>
@Query("UPDATE Students SET is_current = 1 WHERE id = :id")
abstract suspend fun updateCurrent(id: Long)
suspend fun updateCurrent(id: Long)
@Query("UPDATE Students SET is_current = 0")
abstract suspend fun resetCurrent()
@Transaction
open suspend fun switchCurrent(id: Long) {
resetCurrent()
updateCurrent(id)
}
suspend fun resetCurrent()
}

View file

@ -1,37 +0,0 @@
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

@ -47,7 +47,4 @@ data class Attendance(
@PrimaryKey(autoGenerate = true)
var id: Long = 0
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
}

View file

@ -40,7 +40,4 @@ data class Homework(
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
@ColumnInfo(name = "is_added_by_user")
var isAddedByUser: Boolean = false
}

View file

@ -1,27 +0,0 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.github.wulkanowy.services.sync.notifications.NotificationType
import java.time.LocalDateTime
@Entity(tableName = "Notifications")
data class Notification(
@ColumnInfo(name = "student_id")
val studentId: Long,
val title: String,
val content: String,
val type: NotificationType,
val date: LocalDateTime,
val data: String? = null
) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View file

@ -3,9 +3,10 @@ package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.squareup.moshi.JsonClass
import java.io.Serializable
@kotlinx.serialization.Serializable
@JsonClass(generateAdapter = true)
@Entity(tableName = "Recipients")
data class Recipient(

View file

@ -50,7 +50,4 @@ data class Timetable(
@PrimaryKey(autoGenerate = true)
var id: Long = 0
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
}

View file

@ -1,23 +0,0 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration40 : Migration(39, 40) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Notifications` (
`student_id` INTEGER NOT NULL,
`title` TEXT NOT NULL,
`content` TEXT NOT NULL,
`type` TEXT NOT NULL,
`date` INTEGER NOT NULL,
`data` TEXT,
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
)
"""
)
}
}

View file

@ -1,21 +0,0 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
class Migration41(private val sharedPrefProvider: SharedPrefProvider) : Migration(40, 41) {
override fun migrate(database: SupportSQLiteDatabase) {
migrateSharedPreferences()
database.execSQL("ALTER TABLE Homework ADD COLUMN is_added_by_user INTEGER NOT NULL DEFAULT 0")
}
private fun migrateSharedPreferences() {
if (sharedPrefProvider.getBoolean("pref_key_expand_grade", false)) {
sharedPrefProvider.putString("pref_key_expand_grade_mode", GradeExpandMode.ALWAYS_EXPANDED.value)
}
sharedPrefProvider.delete("pref_key_expand_grade")
}
}

View file

@ -1,24 +0,0 @@
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

@ -1,12 +0,0 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration43 : Migration(42, 43) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Timetable ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1")
database.execSQL("ALTER TABLE Attendance ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1")
}
}

View file

@ -1,8 +1,8 @@
package io.github.wulkanowy.data.pojos
import kotlinx.serialization.Serializable
import com.squareup.moshi.JsonClass
@Serializable
@JsonClass(generateAdapter = true)
class Contributor(
val displayName: String,
val githubUsername: String

View file

@ -1,9 +1,9 @@
package io.github.wulkanowy.data.pojos
import com.squareup.moshi.JsonClass
import io.github.wulkanowy.ui.modules.message.send.RecipientChipItem
import kotlinx.serialization.Serializable
@Serializable
@JsonClass(generateAdapter = true)
data class MessageDraft(
val recipients: List<RecipientChipItem>,
val subject: String,

View file

@ -0,0 +1,36 @@
package io.github.wulkanowy.data.pojos
import androidx.annotation.DrawableRes
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import io.github.wulkanowy.services.sync.notifications.NotificationType
import io.github.wulkanowy.ui.modules.main.MainView
sealed interface Notification {
val type: NotificationType
val startMenu: MainView.Section
val icon: Int
val titleStringRes: Int
val contentStringRes: Int
}
data class MultipleNotifications(
override val type: NotificationType,
override val startMenu: MainView.Section,
@DrawableRes override val icon: Int,
@PluralsRes override val titleStringRes: Int,
@PluralsRes override val contentStringRes: Int,
@PluralsRes val summaryStringRes: Int,
val lines: List<String>,
) : Notification
data class OneNotification(
override val type: NotificationType,
override val startMenu: MainView.Section,
@DrawableRes override val icon: Int,
@StringRes override val titleStringRes: Int,
@StringRes override val contentStringRes: Int,
val contentValues: List<String>,
) : Notification

View file

@ -1,19 +0,0 @@
package io.github.wulkanowy.data.pojos
import android.content.Intent
import io.github.wulkanowy.services.sync.notifications.NotificationType
data class NotificationData(
val intentToStart: Intent,
val title: String,
val content: String
)
data class GroupNotificationData(
val notificationDataList: List<NotificationData>,
val title: String,
val content: String,
val intentToStart: Intent,
val type: NotificationType
)

View file

@ -1,52 +0,0 @@
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.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(cacheKey) || 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,27 +1,25 @@
package io.github.wulkanowy.data.repositories
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import android.content.res.AssetManager
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import io.github.wulkanowy.data.pojos.Contributor
import io.github.wulkanowy.utils.DispatchersProvider
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AppCreatorRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val dispatchers: DispatchersProvider,
private val json: Json,
private val assets: AssetManager,
private val dispatchers: DispatchersProvider
) {
@OptIn(ExperimentalSerializationApi::class)
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun getAppCreators() = withContext(dispatchers.io) {
val inputStream = context.assets.open("contributors.json").buffered()
json.decodeFromStream<List<Contributor>>(inputStream)
suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) {
val moshi = Moshi.Builder().build()
val type = Types.newParameterizedType(List::class.java, Contributor::class.java)
val adapter = moshi.adapter<List<Contributor>>(type)
adapter.fromJson(assets.open("contributors.json").bufferedReader().use { it.readText() })
}
}

View file

@ -14,7 +14,6 @@ import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import java.time.LocalDateTime
@ -33,24 +32,10 @@ class AttendanceRepository @Inject constructor(
private val cacheKey = "attendance"
fun getAttendance(
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource(
fun getAttendance(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, semester, start, end)
)
it.isEmpty() || forceRefresh || isExpired
},
query = {
attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
},
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getAttendance(start.monday, end.sunday, semester.semesterId)
@ -58,39 +43,19 @@ class AttendanceRepository @Inject constructor(
},
saveFetchResult = { old, new ->
attendanceDb.deleteAll(old uniqueSubtract new)
val attendanceToAdd = (new uniqueSubtract old).map { newAttendance ->
newAttendance.apply { if (notify) isNotified = false }
}
attendanceDb.insertAll(attendanceToAdd)
attendanceDb.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
},
filterResult = { it.filter { item -> item.date in start..end } }
)
fun getAttendanceFromDatabase(
semester: Semester,
start: LocalDate,
end: LocalDate
): Flow<List<Attendance>> {
return attendanceDb.loadAll(semester.diaryId, semester.studentId, start, end)
}
suspend fun updateTimetable(timetable: List<Attendance>) {
return attendanceDb.updateAll(timetable)
}
suspend fun excuseForAbsence(
student: Student, semester: Semester,
absenceList: List<Attendance>, reason: String? = null
) {
val items = absenceList.map { attendance ->
suspend fun excuseForAbsence(student: Student, semester: Semester, absenceList: List<Attendance>, reason: String? = null) {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).excuseForAbsence(absenceList.map { attendance ->
Absent(
date = LocalDateTime.of(attendance.date, LocalTime.of(0, 0)),
timeId = attendance.timeId
)
}
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.excuseForAbsence(items, reason)
}, reason)
}
}

View file

@ -29,12 +29,12 @@ class AttendanceSummaryRepository @Inject constructor(
student: Student,
semester: Semester,
subjectId: Int,
forceRefresh: Boolean,
forceRefresh: Boolean
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
it.isEmpty() || forceRefresh
|| refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
},
query = { attendanceDb.loadAll(semester.diaryId, semester.studentId, subjectId) },
fetch = {

View file

@ -28,28 +28,10 @@ class CompletedLessonsRepository @Inject constructor(
private val cacheKey = "completed"
fun getCompletedLessons(
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
) = networkBoundResource(
fun getCompletedLessons(student: Student, semester: Semester, start: LocalDate, end: LocalDate, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, semester, start, end)
)
it.isEmpty() || forceRefresh || isExpired
},
query = {
completedLessonsDb.loadAll(
studentId = semester.studentId,
diaryId = semester.diaryId,
from = start.monday,
end = end.sunday
)
},
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester, start, end)) },
query = { completedLessonsDb.loadAll(semester.studentId, semester.diaryId, start.monday, end.sunday) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getCompletedLessons(start.monday, end.sunday)

View file

@ -35,12 +35,12 @@ class ConferenceRepository @Inject constructor(
semester: Semester,
forceRefresh: Boolean,
notify: Boolean = false,
startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC),
startDate: LocalDateTime = LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC)
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
it.isEmpty() || forceRefresh
|| refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
},
query = {
conferenceDb.loadAll(semester.diaryId, student.studentId, startDate)

View file

@ -36,14 +36,14 @@ class ExamRepository @Inject constructor(
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
notify: Boolean = false,
notify: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(
key = getRefreshKey(cacheKey, semester, start, end)
)
it.isEmpty() || forceRefresh || isExpired
it.isEmpty() || forceRefresh || isShouldBeRefreshed
},
query = {
examDb.loadAll(

View file

@ -37,12 +37,13 @@ class GradeRepository @Inject constructor(
student: Student,
semester: Semester,
forceRefresh: Boolean,
notify: Boolean = false,
notify: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { (details, summaries) ->
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
details.isEmpty() || summaries.isEmpty() || forceRefresh || isExpired
val isShouldBeRefreshed =
refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester))
details.isEmpty() || summaries.isEmpty() || forceRefresh || isShouldBeRefreshed
},
query = {
val detailsFlow = gradeDb.loadAll(semester.semesterId, semester.studentId)
@ -70,8 +71,8 @@ class GradeRepository @Inject constructor(
newDetails: List<Grade>,
notify: Boolean
) {
val notifyBreakDate = oldGrades.maxByOrNull {it.date }
?.date ?: student.registrationDate.toLocalDate()
val notifyBreakDate =
oldGrades.maxByOrNull { it.date }?.date ?: student.registrationDate.toLocalDate()
gradeDb.deleteAll(oldGrades uniqueSubtract newDetails)
gradeDb.insertAll((newDetails uniqueSubtract oldGrades).onEach {
if (it.date >= notifyBreakDate) it.apply {
@ -88,7 +89,8 @@ class GradeRepository @Inject constructor(
) {
gradeSummaryDb.deleteAll(oldSummaries uniqueSubtract newSummary)
gradeSummaryDb.insertAll((newSummary uniqueSubtract oldSummaries).onEach { summary ->
val oldSummary = oldSummaries.find { old -> old.subject == summary.subject }
val oldSummary =
oldSummaries.find { oldSummary -> oldSummary.subject == summary.subject }
summary.isPredictedGradeNotified = when {
summary.predictedGrade.isEmpty() -> true
notify && oldSummary?.predictedGrade != summary.predictedGrade -> false

View file

@ -39,19 +39,9 @@ class GradeStatisticsRepository @Inject constructor(
private val semesterCacheKey = "grade_stats_semester"
private val pointsCacheKey = "grade_stats_points"
fun getGradesPartialStatistics(
student: Student,
semester: Semester,
subjectName: String,
forceRefresh: Boolean,
) = networkBoundResource(
fun getGradesPartialStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
mutex = partialMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(partialCacheKey, semester)
)
it.isEmpty() || forceRefresh || isExpired
},
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(partialCacheKey, semester)) },
query = { gradePartialStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -86,19 +76,9 @@ class GradeStatisticsRepository @Inject constructor(
}
)
fun getGradesSemesterStatistics(
student: Student,
semester: Semester,
subjectName: String,
forceRefresh: Boolean,
) = networkBoundResource(
fun getGradesSemesterStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
mutex = semesterMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(semesterCacheKey, semester)
)
it.isEmpty() || forceRefresh || isExpired
},
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(semesterCacheKey, semester)) },
query = { gradeSemesterStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -114,15 +94,13 @@ class GradeStatisticsRepository @Inject constructor(
val itemsWithAverage = items.map { item ->
item.copy().apply {
val denominator = item.amounts.sum()
average = if (denominator == 0) "" else {
(item.amounts.mapIndexed { gradeValue, amount ->
average = if (denominator == 0) "" else (item.amounts.mapIndexed { gradeValue, amount ->
(gradeValue + 1) * amount
}.sum().toDouble() / denominator).let {
"%.2f".format(Locale.FRANCE, it)
}
}
}
}
when (subjectName) {
"Wszystkie" -> (itemsWithAverage.reversed() + GradeSemesterStatistics(
studentId = semester.studentId,
@ -131,9 +109,7 @@ class GradeStatisticsRepository @Inject constructor(
amounts = itemsWithAverage.map { it.amounts }.sumGradeAmounts(),
studentGrade = 0
).apply {
average = itemsWithAverage.mapNotNull {
it.average.replace(",", ".").toDoubleOrNull()
}.average().let {
average = itemsWithAverage.mapNotNull { it.average.replace(",", ".").toDoubleOrNull() }.average().let {
"%.2f".format(Locale.FRANCE, it)
}
}).reversed()
@ -142,17 +118,9 @@ class GradeStatisticsRepository @Inject constructor(
}
)
fun getGradesPointsStatistics(
student: Student,
semester: Semester,
subjectName: String,
forceRefresh: Boolean,
) = networkBoundResource(
fun getGradesPointsStatistics(student: Student, semester: Semester, subjectName: String, forceRefresh: Boolean) = networkBoundResource(
mutex = pointsMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(pointsCacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
},
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(pointsCacheKey, semester)) },
query = { gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)

View file

@ -30,19 +30,16 @@ class HomeworkRepository @Inject constructor(
private val cacheKey = "homework"
fun getHomework(
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
notify: Boolean = false,
student: Student, semester: Semester,
start: LocalDate, end: LocalDate,
forceRefresh: Boolean, notify: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
val isShouldBeRefreshed = refreshHelper.isShouldBeRefreshed(
key = getRefreshKey(cacheKey, semester, start, end)
)
it.isEmpty() || forceRefresh || isExpired
it.isEmpty() || forceRefresh || isShouldBeRefreshed
},
query = {
homeworkDb.loadAll(
@ -61,9 +58,8 @@ class HomeworkRepository @Inject constructor(
val homeWorkToSave = (new uniqueSubtract old).onEach {
if (notify) it.isNotified = false
}
val filteredOld = old.filterNot { it.isAddedByUser }
homeworkDb.deleteAll(filteredOld uniqueSubtract new)
homeworkDb.deleteAll(old uniqueSubtract new)
homeworkDb.insertAll(homeWorkToSave)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
@ -80,8 +76,4 @@ class HomeworkRepository @Inject constructor(
homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday)
suspend fun updateHomework(homework: List<Homework>) = homeworkDb.updateAll(homework)
suspend fun saveHomework(homework: Homework) = homeworkDb.insertAll(listOf(homework))
suspend fun deleteHomework(homework: Homework) = homeworkDb.deleteAll(listOf(homework))
}

View file

@ -15,23 +15,24 @@ class LoggerRepository @Inject constructor(
suspend fun getLastLogLines() = getLastModified().readText().split("\n")
suspend fun getLogFiles() = withContext(dispatchers.io) {
File(context.filesDir.absolutePath).listFiles(File::isFile)
?.filter { it.name.endsWith(".log") }!!
suspend fun getLogFiles() = withContext(dispatchers.backgroundThread) {
File(context.filesDir.absolutePath).listFiles(File::isFile)?.filter {
it.name.endsWith(".log")
}!!
}
private suspend fun getLastModified() = withContext(dispatchers.io) {
private suspend fun getLastModified(): File {
return withContext(dispatchers.backgroundThread) {
var lastModifiedTime = Long.MIN_VALUE
var chosenFile: File? = null
File(context.filesDir.absolutePath).listFiles(File::isFile)
?.forEach { file ->
File(context.filesDir.absolutePath).listFiles(File::isFile)?.forEach { file ->
if (file.lastModified() > lastModifiedTime) {
lastModifiedTime = file.lastModified()
chosenFile = file
}
}
chosenFile ?: throw FileNotFoundException("Log file not found")
if (chosenFile == null) throw FileNotFoundException("Log file not found")
chosenFile!!
}
}
}

View file

@ -23,17 +23,11 @@ class LuckyNumberRepository @Inject constructor(
private val saveFetchResultMutex = Mutex()
fun getLuckyNumber(
student: Student,
forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource(
fun getLuckyNumber(student: Student, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { luckyNumberDb.load(student.studentId, now()) },
fetch = {
sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student)
},
fetch = { sdk.init(student).getLuckyNumber(student.schoolShortName)?.mapToEntity(student) },
saveFetchResult = { old, new ->
if (new != old) {
old?.let { luckyNumberDb.deleteAll(listOfNotNull(it)) }
@ -47,11 +41,9 @@ class LuckyNumberRepository @Inject constructor(
fun getLuckyNumberHistory(student: Student, start: LocalDate, end: LocalDate) =
luckyNumberDb.getAll(student.studentId, start, end)
suspend fun getNotNotifiedLuckyNumber(student: Student) =
luckyNumberDb.load(student.studentId, now()).map {
suspend fun getNotNotifiedLuckyNumber(student: Student) = luckyNumberDb.load(student.studentId, now()).map {
if (it?.isNotified == false) it else null
}.first()
suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) =
luckyNumberDb.updateAll(listOfNotNull(luckyNumber))
suspend fun updateLuckyNumber(luckyNumber: LuckyNumber?) = luckyNumberDb.updateAll(listOfNotNull(luckyNumber))
}

View file

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.repositories
import android.content.Context
import com.squareup.moshi.Moshi
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.Resource
@ -17,6 +18,7 @@ import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.data.pojos.MessageDraftJsonAdapter
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Folder
import io.github.wulkanowy.sdk.pojo.SentMessage
@ -27,9 +29,6 @@ import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import timber.log.Timber
import java.time.LocalDateTime.now
import javax.inject.Inject
@ -43,7 +42,7 @@ class MessageRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val refreshHelper: AutoRefreshHelper,
private val sharedPrefProvider: SharedPrefProvider,
private val json: Json,
private val moshi: Moshi,
) {
private val saveFetchResultMutex = Mutex()
@ -52,18 +51,14 @@ class MessageRepository @Inject constructor(
@Suppress("UNUSED_PARAMETER")
fun getMessages(
student: Student,
semester: Semester,
folder: MessageFolder,
forceRefresh: Boolean,
notify: Boolean = false,
student: Student, semester: Semester,
folder: MessageFolder, forceRefresh: Boolean, notify: Boolean = false
): Flow<Resource<List<Message>>> = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, student, folder)
it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(
getRefreshKey(cacheKey, student, folder)
)
it.isEmpty() || forceRefresh || isExpired
},
query = { messagesDb.loadAll(student.id.toInt(), folder.id) },
fetch = {
@ -82,8 +77,7 @@ class MessageRepository @Inject constructor(
)
private fun getMessagesWithReadByChange(
old: List<Message>,
new: List<Message>,
old: List<Message>, new: List<Message>,
setNotified: Boolean
): List<Message> {
val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) }
@ -102,9 +96,7 @@ class MessageRepository @Inject constructor(
}
fun getMessage(
student: Student,
message: Message,
markAsRead: Boolean = false,
student: Student, message: Message, markAsRead: Boolean = false
): Flow<Resource<MessageWithAttachment?>> = networkBoundResource(
shouldFetch = {
checkNotNull(it, { "This message no longer exist!" })
@ -143,10 +135,8 @@ class MessageRepository @Inject constructor(
}
suspend fun sendMessage(
student: Student,
subject: String,
content: String,
recipients: List<Recipient>,
student: Student, subject: String, content: String,
recipients: List<Recipient>
): SentMessage = sdk.init(student).sendMessage(
subject = subject,
content = content,
@ -169,9 +159,9 @@ class MessageRepository @Inject constructor(
var draftMessage: MessageDraft?
get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft))
?.let { json.decodeFromString(it) }
?.let { MessageDraftJsonAdapter(moshi).fromJson(it) }
set(value) = sharedPrefProvider.putString(
context.getString(R.string.pref_key_message_send_draft),
value?.let { json.encodeToString(it) }
value?.let { MessageDraftJsonAdapter(moshi).toJson(it) }
)
}

View file

@ -28,16 +28,9 @@ class MobileDeviceRepository @Inject constructor(
private val cacheKey = "devices"
fun getDevices(
student: Student,
semester: Semester,
forceRefresh: Boolean,
) = networkBoundResource(
fun getDevices(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
it.isEmpty() || forceRefresh || isExpired
},
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student)) },
query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)

View file

@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -27,19 +28,9 @@ class NoteRepository @Inject constructor(
private val cacheKey = "note"
fun getNotes(
student: Student,
semester: Semester,
forceRefresh: Boolean,
notify: Boolean = false,
) = networkBoundResource(
fun getNotes(student: Student, semester: Semester, forceRefresh: Boolean, notify: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
getRefreshKey(cacheKey, semester)
)
it.isEmpty() || forceRefresh || isExpired
},
shouldFetch = { it.isEmpty() || forceRefresh || refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, semester)) },
query = { noteDb.loadAll(student.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)

View file

@ -1,17 +0,0 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.NotificationDao
import io.github.wulkanowy.data.db.entities.Notification
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NotificationRepository @Inject constructor(
private val notificationDao: NotificationDao,
) {
fun getNotifications(studentId: Long) = notificationDao.loadAll(studentId)
suspend fun saveNotification(notification: Notification) =
notificationDao.insertAll(listOf(notification))
}

View file

@ -5,23 +5,21 @@ import android.content.SharedPreferences
import androidx.core.content.edit
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.fredporciuncula.flow.preferences.Preference
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapter
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.sdk.toLocalDate
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode
import io.github.wulkanowy.utils.toTimestamp
import io.github.wulkanowy.utils.toLocalDateTime
import io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.lang.ClassCastException
import java.lang.IllegalStateException
import java.time.LocalDate
import java.time.LocalDateTime
import javax.inject.Inject
@ -30,12 +28,16 @@ import javax.inject.Singleton
@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
class PreferencesRepository @Inject constructor(
@ApplicationContext val context: Context,
private val sharedPref: SharedPreferences,
private val flowSharedPref: FlowSharedPreferences,
private val json: Json,
@ApplicationContext val context: Context,
moshi: Moshi
) {
@OptIn(ExperimentalStdlibApi::class)
private val dashboardItemsPositionAdapter: JsonAdapter<Map<DashboardItem.Type, Int>> =
moshi.adapter()
val startMenuIndex: Int
get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt()
@ -59,13 +61,8 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_grade_average_force_calc
)
val gradeExpandMode: GradeExpandMode
get() = GradeExpandMode.getByValue(
getString(
R.string.pref_key_expand_grade_mode,
R.string.pref_default_expand_grade_mode
)
)
val isGradeExpandable: Boolean
get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade)
val showAllSubjectsOnStatisticsList: Boolean
get() = getBoolean(
@ -111,22 +108,6 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_notification_upcoming_lessons_enable
)
val isUpcomingLessonsNotificationsPersistentKey =
context.getString(R.string.pref_key_notifications_upcoming_lessons_persistent)
val isUpcomingLessonsNotificationsPersistent: Boolean
get() = getBoolean(
isUpcomingLessonsNotificationsPersistentKey,
R.bool.pref_default_notification_upcoming_lessons_persistent
)
val isNotificationPiggybackEnabledKey =
context.getString(R.string.pref_key_notifications_piggyback)
val isNotificationPiggybackEnabled: Boolean
get() = getBoolean(
R.string.pref_key_notifications_piggyback,
R.bool.pref_default_notification_piggyback
)
val isDebugNotificationEnableKey = context.getString(R.string.pref_key_notification_debug)
val isDebugNotificationEnable: Boolean
get() = getBoolean(isDebugNotificationEnableKey, R.bool.pref_default_notification_debug)
@ -195,20 +176,22 @@ class PreferencesRepository @Inject constructor(
)
var lasSyncDate: LocalDateTime
get() = getLong(R.string.pref_key_last_sync_date, R.string.pref_default_last_sync_date)
.toLocalDateTime()
get() = getLong(
R.string.pref_key_last_sync_date,
R.string.pref_default_last_sync_date
).toLocalDateTime()
set(value) = sharedPref.edit().putLong("last_sync_date", value.toTimestamp()).apply()
var dashboardItemsPosition: Map<DashboardItem.Type, Int>?
get() {
val value = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null
val json = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null
return json.decodeFromString(value)
return dashboardItemsPositionAdapter.fromJson(json)
}
set(value) = sharedPref.edit {
putString(
PREF_KEY_DASHBOARD_ITEMS_POSITION,
json.encodeToString(value)
dashboardItemsPositionAdapter.toJson(value)
)
}
@ -217,7 +200,6 @@ class PreferencesRepository @Inject constructor(
.map { set ->
set.map { DashboardItem.Tile.valueOf(it) }
.plus(DashboardItem.Tile.ACCOUNT)
.plus(DashboardItem.Tile.ADMIN_MESSAGE)
.toSet()
}
@ -225,7 +207,6 @@ 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 }
@ -249,10 +230,8 @@ class PreferencesRepository @Inject constructor(
set(value) = sharedPref.edit().putInt(PREF_KEY_IN_APP_REVIEW_COUNT, value).apply()
var inAppReviewDate: LocalDate?
get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L }
?.toLocalDate()
set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp())
.apply()
get() = sharedPref.getLong(PREF_KEY_IN_APP_REVIEW_DATE, 0).takeIf { it != 0L }?.toLocalDate()
set(value) = sharedPref.edit().putLong(PREF_KEY_IN_APP_REVIEW_DATE, value!!.toTimestamp()).apply()
var isAppReviewDone: Boolean
get() = sharedPref.getBoolean(PREF_KEY_IN_APP_REVIEW_DONE, false)
@ -273,9 +252,6 @@ class PreferencesRepository @Inject constructor(
private fun getBoolean(id: String, default: Int) =
sharedPref.getBoolean(id, context.resources.getBoolean(default))
private fun getBoolean(id: Int, default: Boolean) =
sharedPref.getBoolean(context.getString(id), default)
private companion object {
private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position"

View file

@ -7,8 +7,6 @@ import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import javax.inject.Inject
@ -17,34 +15,26 @@ import javax.inject.Singleton
@Singleton
class RecipientRepository @Inject constructor(
private val recipientDb: RecipientDao,
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
private val sdk: Sdk
) {
private val cacheKey = "recipient"
suspend fun refreshRecipients(student: Student, unit: ReportingUnit, role: Int) {
val new = sdk.init(student).getRecipients(unit.unitId, role).mapToEntities(unit.studentId)
val old = recipientDb.loadAll(unit.studentId, unit.unitId, role)
recipientDb.deleteAll(old uniqueSubtract new)
recipientDb.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
suspend fun getRecipients(student: Student, unit: ReportingUnit, role: Int): List<Recipient> {
val cached = recipientDb.loadAll(unit.studentId, unit.unitId, role)
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
return if (cached.isEmpty() || isExpired) {
return recipientDb.loadAll(unit.studentId, unit.unitId, role).ifEmpty {
refreshRecipients(student, unit, role)
recipientDb.loadAll(unit.studentId, unit.unitId, role)
} else cached
}
}
suspend fun getMessageRecipients(student: Student, message: Message): List<Recipient> {
return sdk.init(student).getMessageRecipients(message.messageId, message.senderId)
.mapToEntities(student.studentId)
return sdk.init(student).getMessageRecipients(message.messageId, message.senderId).mapToEntities(student.studentId)
}
}

View file

@ -11,7 +11,7 @@ class RecoverRepository @Inject constructor(private val sdk: Sdk) {
return sdk.getPasswordResetCaptchaCode(host, symbol)
}
suspend fun sendRecoverRequest(
url: String, symbol: String, email: String, reCaptchaResponse: String
): String = sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
suspend fun sendRecoverRequest(url: String, symbol: String, email: String, reCaptchaResponse: String): String {
return sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
}
}

View file

@ -2,6 +2,7 @@ package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.SchoolAnnouncementDao
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
@ -11,6 +12,7 @@ import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@ -28,15 +30,17 @@ class SchoolAnnouncementRepository @Inject constructor(
fun getSchoolAnnouncements(
student: Student,
forceRefresh: Boolean, notify: Boolean = false
forceRefresh: Boolean,
notify: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
it.isEmpty() || forceRefresh || isExpired
it.isEmpty() || forceRefresh
|| refreshHelper.isShouldBeRefreshed(getRefreshKey(cacheKey, student))
},
query = {
schoolAnnouncementDb.loadAll(student.studentId)
schoolAnnouncementDb.loadAll(
student.studentId)
},
fetch = {
sdk.init(student)
@ -53,11 +57,9 @@ class SchoolAnnouncementRepository @Inject constructor(
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
)
fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> {
return schoolAnnouncementDb.loadAll(student.studentId)
}
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) =
schoolAnnouncementDb.updateAll(schoolAnnouncement)
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) = schoolAnnouncementDb.updateAll(schoolAnnouncement)
}

View file

@ -5,8 +5,6 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import kotlinx.coroutines.sync.Mutex
@ -16,26 +14,15 @@ import javax.inject.Singleton
@Singleton
class SchoolRepository @Inject constructor(
private val schoolDb: SchoolDao,
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "school_info"
fun getSchoolInfo(
student: Student,
semester: Semester,
forceRefresh: Boolean,
) = networkBoundResource(
fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey, student)
)
it == null || forceRefresh || isExpired
},
shouldFetch = { it == null || forceRefresh },
query = { schoolDb.load(semester.studentId, semester.classId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool()
@ -50,7 +37,6 @@ class SchoolRepository @Inject constructor(
} else if (old == null) {
schoolDb.insertAll(listOf(new))
}
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
)
}

View file

@ -26,7 +26,7 @@ class SemesterRepository @Inject constructor(
student: Student,
forceRefresh: Boolean = false,
refreshOnNoCurrent: Boolean = false
) = withContext(dispatchers.io) {
) = withContext(dispatchers.backgroundThread) {
val semesters = semesterDb.loadAll(student.studentId, student.classId)
if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) {
@ -64,7 +64,7 @@ class SemesterRepository @Inject constructor(
}
suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) =
withContext(dispatchers.io) {
withContext(dispatchers.backgroundThread) {
getSemesters(student, forceRefresh).getCurrentOrLast()
}
}

View file

@ -19,11 +19,8 @@ class StudentInfoRepository @Inject constructor(
private val saveFetchResultMutex = Mutex()
fun getStudentInfo(
student: Student,
semester: Semester,
forceRefresh: Boolean,
) = networkBoundResource(
fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { it == null || forceRefresh },
query = { studentInfoDao.loadStudentInfo(student.studentId) },

View file

@ -1,9 +1,7 @@
package io.github.wulkanowy.data.repositories
import android.content.Context
import androidx.room.withTransaction
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Student
@ -27,8 +25,7 @@ class StudentRepository @Inject constructor(
private val studentDb: StudentDao,
private val semesterDb: SemesterDao,
private val sdk: Sdk,
private val appInfo: AppInfo,
private val appDatabase: AppDatabase
private val appInfo: AppInfo
) {
suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty()
@ -66,7 +63,7 @@ class StudentRepository @Inject constructor(
.map {
it.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = withContext(dispatchers.io) {
student.password = withContext(dispatchers.backgroundThread) {
decrypt(student.password)
}
}
@ -77,7 +74,7 @@ class StudentRepository @Inject constructor(
val student = studentDb.loadById(id) ?: throw NoCurrentStudentException()
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = withContext(dispatchers.io) {
student.password = withContext(dispatchers.backgroundThread) {
decrypt(student.password)
}
}
@ -88,40 +85,35 @@ class StudentRepository @Inject constructor(
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = withContext(dispatchers.io) {
student.password = withContext(dispatchers.backgroundThread) {
decrypt(student.password)
}
}
return student
}
suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>) {
suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>): List<Long> {
val semesters = studentsWithSemesters.flatMap { it.semesters }
val students = studentsWithSemesters.map { it.student }
.map {
it.apply {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) {
password = withContext(dispatchers.io) {
password = withContext(dispatchers.backgroundThread) {
encrypt(password, context)
}
}
}
}
.mapIndexed { index, student ->
if (index == 0) {
student.copy(isCurrent = true).apply { avatarColor = student.avatarColor }
} else student
}
appDatabase.withTransaction {
studentDb.resetCurrent()
semesterDb.insertSemesters(semesters)
studentDb.insertAll(students)
}
return studentDb.insertAll(students)
}
suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) {
studentDb.switchCurrent(studentWithSemesters.student.id)
with(studentDb) {
resetCurrent()
updateCurrent(studentWithSemesters.student.id)
}
}
suspend fun logoutStudent(student: Student) = studentDb.delete(student)

View file

@ -5,8 +5,6 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
@ -17,24 +15,14 @@ import javax.inject.Singleton
@Singleton
class SubjectRepository @Inject constructor(
private val subjectDao: SubjectDao,
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "subjects"
fun getSubjects(
student: Student,
semester: Semester,
forceRefresh: Boolean = false,
) = networkBoundResource(
fun getSubjects(student: Student, semester: Semester, forceRefresh: Boolean = false) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
},
shouldFetch = { it.isEmpty() || forceRefresh },
query = { subjectDao.loadAll(semester.diaryId, semester.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -43,8 +31,6 @@ class SubjectRepository @Inject constructor(
saveFetchResult = { old, new ->
subjectDao.deleteAll(old uniqueSubtract new)
subjectDao.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)
}

View file

@ -5,8 +5,6 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import io.github.wulkanowy.utils.uniqueSubtract
@ -17,24 +15,14 @@ import javax.inject.Singleton
@Singleton
class TeacherRepository @Inject constructor(
private val teacherDb: TeacherDao,
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
private val sdk: Sdk
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "teachers"
fun getTeachers(
student: Student,
semester: Semester,
forceRefresh: Boolean,
) = networkBoundResource(
fun getTeachers(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
},
shouldFetch = { it.isEmpty() || forceRefresh },
query = { teacherDb.loadAll(semester.studentId, semester.classId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
@ -44,8 +32,6 @@ class TeacherRepository @Inject constructor(
saveFetchResult = { old, new ->
teacherDb.deleteAll(old uniqueSubtract new)
teacherDb.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester))
}
)
}

View file

@ -41,23 +41,18 @@ class TimetableRepository @Inject constructor(
private val cacheKey = "timetable"
fun getTimetable(
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
forceRefresh: Boolean,
refreshAdditional: Boolean = false,
notify: Boolean = false
student: Student, semester: Semester, start: LocalDate, end: LocalDate,
forceRefresh: Boolean, refreshAdditional: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
shouldFetch = { (timetable, additional, headers) ->
val refreshKey = getRefreshKey(cacheKey, semester, start, end)
val isExpired = refreshHelper.shouldBeRefreshed(refreshKey)
val isShouldRefresh = refreshHelper.isShouldBeRefreshed(refreshKey)
val isRefreshAdditional = additional.isEmpty() && refreshAdditional
val isNoData = timetable.isEmpty() || isRefreshAdditional || headers.isEmpty()
isNoData || forceRefresh || isExpired
isNoData || forceRefresh || isShouldRefresh
},
query = { getFullTimetableFromDatabase(student, semester, start, end) },
fetch = {
@ -68,7 +63,7 @@ class TimetableRepository @Inject constructor(
timetableFull.mapToEntities(semester)
},
saveFetchResult = { timetableOld, timetableNew ->
refreshTimetable(student, timetableOld.lessons, timetableNew.lessons, notify)
refreshTimetable(student, timetableOld.lessons, timetableNew.lessons)
refreshAdditional(timetableOld.additional, timetableNew.additional)
refreshDayHeaders(timetableOld.headers, timetableNew.headers)
@ -84,10 +79,8 @@ class TimetableRepository @Inject constructor(
)
private fun getFullTimetableFromDatabase(
student: Student,
semester: Semester,
start: LocalDate,
end: LocalDate,
student: Student, semester: Semester,
start: LocalDate, end: LocalDate
): Flow<TimetableFull> {
val timetableFlow = timetableDb.loadAll(
diaryId = semester.diaryId,
@ -118,27 +111,21 @@ class TimetableRepository @Inject constructor(
}
}
fun getTimetableFromDatabase(
semester: Semester,
from: LocalDate,
end: LocalDate
): Flow<List<Timetable>> {
return timetableDb.loadAll(semester.diaryId, semester.studentId, from, end)
}
suspend fun updateTimetable(timetable: List<Timetable>) {
return timetableDb.updateAll(timetable)
}
private suspend fun refreshTimetable(
student: Student,
lessonsOld: List<Timetable>,
lessonsNew: List<Timetable>,
notify: Boolean
lessonsOld: List<Timetable>, lessonsNew: List<Timetable>
) {
val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew
val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new ->
new.apply { if (notify) isNotified = false }
val matchingOld = lessonsOld.singleOrNull { new.start == it.start }
if (matchingOld != null) {
val useOldTeacher = new.teacher.isEmpty() && !new.changes && !matchingOld.changes
new.copy(
room = if (new.room.isEmpty()) matchingOld.room else new.room,
teacher = if (useOldTeacher) matchingOld.teacher
else new.teacher
)
} else new
}
timetableDb.deleteAll(lessonsToRemove)

View file

@ -15,7 +15,6 @@ import dagger.multibindings.IntoSet
import io.github.wulkanowy.services.sync.channels.Channel
import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel
import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel
import io.github.wulkanowy.services.sync.channels.NewConferencesChannel
import io.github.wulkanowy.services.sync.channels.NewExamChannel
import io.github.wulkanowy.services.sync.channels.NewGradesChannel
@ -24,7 +23,6 @@ import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel
import io.github.wulkanowy.services.sync.channels.PushChannel
import io.github.wulkanowy.services.sync.channels.TimetableChangeChannel
import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel
import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork
import io.github.wulkanowy.services.sync.works.AttendanceWork
@ -169,12 +167,4 @@ abstract class ServicesModule {
@Binds
@IntoSet
abstract fun provideUpcomingLessonsChannel(channel: UpcomingLessonsChannel): Channel
@Binds
@IntoSet
abstract fun provideChangeTimetableChannel(channel: TimetableChangeChannel): Channel
@Binds
@IntoSet
abstract fun provideNewAttendanceChannel(channel: NewAttendanceChannel): Channel
}

View file

@ -1,6 +1,7 @@
package io.github.wulkanowy.services.alarm
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Intent
import android.os.Build
@ -10,13 +11,11 @@ import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.HiltBroadcastReceiver
import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.PendingIntentCompat
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.flowWithResource
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.toLocalDateTime
@ -33,15 +32,12 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
@Inject
lateinit var studentRepository: StudentRepository
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object {
const val NOTIFICATION_TYPE_CURRENT = 1
const val NOTIFICATION_TYPE_UPCOMING = 2
const val NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION = 3
const val NOTIFICATION_ID = 2137
const val NOTIFICATION_ID = "id"
const val STUDENT_NAME = "student_name"
const val STUDENT_ID = "student_id"
@ -71,10 +67,10 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
private fun prepareNotification(context: Context, intent: Intent) {
val type = intent.getIntExtra(LESSON_TYPE, 0)
val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent
val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) {
return NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID)
return NotificationManagerCompat.from(context).cancel(notificationId)
}
val studentId = intent.getIntExtra(STUDENT_ID, 0)
@ -91,39 +87,20 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId")
showNotification(
context, isPersistent, studentName,
showNotification(context, notificationId, studentName,
if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start,
context.getString(
if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next,
"($room) $subject".removePrefix("()")
),
nextSubject?.let {
context.getString(
R.string.timetable_later,
"($nextRoom) $nextSubject".removePrefix("()")
)
}
context.getString(if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next, "($room) $subject".removePrefix("()")),
nextSubject?.let { context.getString(R.string.timetable_later, "($nextRoom) $nextSubject".removePrefix("()")) }
)
}
private fun showNotification(
context: Context,
isPersistent: Boolean,
studentName: String?,
countDown: Long,
timeout: Long,
title: String,
next: String?
) {
NotificationManagerCompat.from(context)
.notify(NOTIFICATION_ID, NotificationCompat.Builder(context, CHANNEL_ID)
private fun showNotification(context: Context, notificationId: Int, studentName: String?, countDown: Long, timeout: Long, title: String, next: String?) {
NotificationManagerCompat.from(context).notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(title)
.setContentText(next)
.setAutoCancel(false)
.setOngoing(true)
.setWhen(countDown)
.setOngoing(isPersistent)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.apply {
if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true)
}
@ -134,14 +111,8 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
it.setSummaryText(studentName)
it.addLine(next)
})
.setContentIntent(
PendingIntent.getActivity(
context,
NOTIFICATION_ID,
SplashActivity.getStartIntent(context, Destination.Timetable()),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
)
.setContentIntent(PendingIntent.getActivity(context, MainView.Section.TIMETABLE.id,
MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true), FLAG_UPDATE_CURRENT))
.build()
)
}

View file

@ -3,6 +3,7 @@ package io.github.wulkanowy.services.alarm
import android.app.AlarmManager
import android.app.AlarmManager.RTC_WAKEUP
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Intent
import androidx.core.app.AlarmManagerCompat
@ -24,13 +25,12 @@ import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companio
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_UPCOMING
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_ID
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.PendingIntentCompat
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalDateTime.now
import javax.inject.Inject
@ -53,17 +53,14 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
suspend fun cancelScheduled(lessons: List<Timetable>, student: Student) {
val studentId = student.studentId
withContext(dispatchersProvider.io) {
withContext(dispatchersProvider.backgroundThread) {
lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
val upcomingTime = getUpcomingLessonTime(index, lessons, lesson)
cancelScheduledTo(
range = upcomingTime..lesson.start,
requestCode = getRequestCode(upcomingTime, studentId)
)
cancelScheduledTo(
range = lesson.start..lesson.end,
requestCode = getRequestCode(lesson.start, studentId)
upcomingTime..lesson.start,
getRequestCode(upcomingTime, studentId)
)
cancelScheduledTo(lesson.start..lesson.end, getRequestCode(lesson.start, studentId))
Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId")
}
@ -72,31 +69,20 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
private fun cancelScheduledTo(range: ClosedRange<LocalDateTime>, requestCode: Int) {
if (now() in range) cancelNotification()
alarmManager.cancel(
PendingIntent.getBroadcast(
context,
requestCode,
Intent(),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_UPDATE_CURRENT)
)
}
fun cancelNotification() =
NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID)
NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id)
suspend fun scheduleNotifications(lessons: List<Timetable>, student: Student) {
if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) {
return cancelScheduled(lessons, student)
}
if (lessons.firstOrNull()?.date?.isAfter(LocalDate.now().plusDays(2)) == true) {
Timber.d("Timetable notification scheduling skipped - lessons are too far")
return
}
withContext(dispatchersProvider.io) {
withContext(dispatchersProvider.backgroundThread) {
lessons.groupBy { it.date }
.map { it.value.sortedBy { lesson -> lesson.start } }
.map { it.filter { lesson -> lesson.isStudentPlan } }
@ -110,26 +96,26 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
if (lesson.start > now()) {
scheduleBroadcast(
intent = intent,
studentId = student.studentId,
notificationType = NOTIFICATION_TYPE_UPCOMING,
time = getUpcomingLessonTime(index, active, lesson)
intent,
student.studentId,
NOTIFICATION_TYPE_UPCOMING,
getUpcomingLessonTime(index, active, lesson)
)
}
if (lesson.end > now()) {
scheduleBroadcast(
intent = intent,
studentId = student.studentId,
notificationType = NOTIFICATION_TYPE_CURRENT,
time = lesson.start
intent,
student.studentId,
NOTIFICATION_TYPE_CURRENT,
lesson.start
)
if (active.lastIndex == index) {
scheduleBroadcast(
intent = intent,
studentId = student.studentId,
notificationType = NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION,
time = lesson.end
intent,
student.studentId,
NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION,
lesson.end
)
}
}
@ -157,20 +143,17 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
notificationType: Int,
time: LocalDateTime
) {
try {
AlarmManagerCompat.setExactAndAllowWhileIdle(
alarmManager, RTC_WAKEUP, time.toTimestamp(),
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
it.putExtra(LESSON_TYPE, notificationType)
}, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE)
}, FLAG_UPDATE_CURRENT)
)
Timber.d(
"TimetableNotification scheduled: type: $notificationType, subject: ${
intent.getStringExtra(LESSON_TITLE)
}, start: $time, student: $studentId"
)
} catch (e: IllegalStateException) {
Timber.e(e)
}
}
}

View file

@ -1,24 +0,0 @@
package io.github.wulkanowy.services.piggyback
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.sync.SyncManager
import javax.inject.Inject
@AndroidEntryPoint
class VulcanNotificationListenerService : NotificationListenerService() {
@Inject
lateinit var syncManager: SyncManager
@Inject
lateinit var preferenceRepository: PreferencesRepository
override fun onNotificationPosted(statusBarNotification: StatusBarNotification?) {
if (statusBarNotification?.packageName == "pl.edu.vulcan.hebe" && preferenceRepository.isNotificationPiggybackEnabled) {
syncManager.startOneTimeSyncWorker()
}
}
}

View file

@ -1,90 +0,0 @@
package io.github.wulkanowy.services.shortcuts
import android.content.Context
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ShortcutsHelper @Inject constructor(@ApplicationContext private val context: Context) {
private val destinations = mapOf(
"grade" to Destination.Grade,
"attendance" to Destination.Attendance,
"exam" to Destination.Exam,
"timetable" to Destination.Timetable()
)
init {
initializeShortcuts()
}
fun getDestination(intent: Intent) =
destinations[intent.getStringExtra(EXTRA_SHORTCUT_DESTINATION_ID)]
private fun initializeShortcuts() {
val shortcutsInfo = listOf(
ShortcutInfoCompat.Builder(context, "grade_shortcut")
.setShortLabel(context.getString(R.string.grade_title))
.setLongLabel(context.getString(R.string.grade_title))
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_grade))
.setIntent(SplashActivity.getStartIntent(context)
.apply {
action = Intent.ACTION_VIEW
putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "grade")
}
)
.build(),
ShortcutInfoCompat.Builder(context, "attendance_shortcut")
.setShortLabel(context.getString(R.string.attendance_title))
.setLongLabel(context.getString(R.string.attendance_title))
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_attendance))
.setIntent(SplashActivity.getStartIntent(context)
.apply {
action = Intent.ACTION_VIEW
putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "attendance")
}
)
.build(),
ShortcutInfoCompat.Builder(context, "exam_shortcut")
.setShortLabel(context.getString(R.string.exam_title))
.setLongLabel(context.getString(R.string.exam_title))
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_exam))
.setIntent(SplashActivity.getStartIntent(context)
.apply {
action = Intent.ACTION_VIEW
putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "exam")
}
)
.build(),
ShortcutInfoCompat.Builder(context, "timetable_shortcut")
.setShortLabel(context.getString(R.string.timetable_title))
.setLongLabel(context.getString(R.string.timetable_title))
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_timetable))
.setIntent(SplashActivity.getStartIntent(context)
.apply {
action = Intent.ACTION_VIEW
putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "timetable")
}
)
.build()
)
shortcutsInfo.forEach { ShortcutManagerCompat.pushDynamicShortcut(context, it) }
}
private companion object {
private const val EXTRA_SHORTCUT_DESTINATION_ID = "shortcut_destination_id"
}
}

View file

@ -57,24 +57,18 @@ class SyncManager @Inject constructor(
fun startPeriodicSyncWorker(restart: Boolean = false) {
if (preferencesRepository.isServiceEnabled && !now().isHolidays) {
val serviceInterval = preferencesRepository.servicesInterval
workManager.enqueueUniquePeriodicWork(
SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP,
PeriodicWorkRequestBuilder<SyncWorker>(serviceInterval, MINUTES)
workManager.enqueueUniquePeriodicWork(SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP,
PeriodicWorkRequestBuilder<SyncWorker>(preferencesRepository.servicesInterval, MINUTES)
.setInitialDelay(10, MINUTES)
.setBackoffCriteria(EXPONENTIAL, 30, MINUTES)
.setConstraints(
Constraints.Builder()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(if (preferencesRepository.isServicesOnlyWifi) UNMETERED else CONNECTED)
.build()
)
.build()
)
.build())
.build())
}
}
fun startOneTimeSyncWorker(): Flow<WorkInfo?> {
fun startOneTimeSyncWorker(): Flow<WorkInfo> {
val work = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(
Data.Builder()
@ -83,11 +77,7 @@ class SyncManager @Inject constructor(
)
.build()
workManager.enqueueUniqueWork(
"${SyncWorker::class.java.simpleName}_one_time",
ExistingWorkPolicy.REPLACE,
work
)
workManager.enqueueUniqueWork("${SyncWorker::class.java.simpleName}_one_time", ExistingWorkPolicy.REPLACE, work)
return workManager.getWorkInfoByIdLiveData(work.id).asFlow()
}

View file

@ -19,11 +19,11 @@ import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.works.Work
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.getCompatColor
import kotlinx.coroutines.withContext
import kotlinx.coroutines.coroutineScope
import timber.log.Timber
import java.time.LocalDateTime
import java.time.ZoneId
import kotlin.random.Random
@HiltWorker
@ -34,14 +34,13 @@ class SyncWorker @AssistedInject constructor(
private val semesterRepository: SemesterRepository,
private val works: Set<@JvmSuppressWildcards Work>,
private val preferencesRepository: PreferencesRepository,
private val notificationManager: NotificationManagerCompat,
private val dispatchersProvider: DispatchersProvider
private val notificationManager: NotificationManagerCompat
) : CoroutineWorker(appContext, workerParameters) {
override suspend fun doWork() = withContext(dispatchersProvider.io) {
override suspend fun doWork() = coroutineScope {
Timber.i("SyncWorker is starting")
if (!studentRepository.isCurrentStudentSet()) return@withContext Result.failure()
if (!studentRepository.isCurrentStudentSet()) return@coroutineScope Result.failure()
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student, true)
@ -51,12 +50,12 @@ class SyncWorker @AssistedInject constructor(
Timber.i("${work::class.java.simpleName} is starting")
work.doWork(student, semester)
Timber.i("${work::class.java.simpleName} result: Success")
preferencesRepository.lasSyncDate = LocalDateTime.now(ZoneId.systemDefault())
null
} catch (e: Throwable) {
Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred")
if (e is FeatureDisabledException || e is FeatureNotAvailableException) {
null
} else {
if (e is FeatureDisabledException || e is FeatureNotAvailableException) null
else {
Timber.e(e)
e
}
@ -71,16 +70,13 @@ class SyncWorker @AssistedInject constructor(
)
}
exceptions.isNotEmpty() -> Result.retry()
else -> {
preferencesRepository.lasSyncDate = LocalDateTime.now()
Result.success()
}
else -> Result.success()
}
if (preferencesRepository.isDebugNotificationEnable) notify(result)
Timber.i("SyncWorker result: $result")
return@withContext result
result
}
private fun notify(result: Result) {

View file

@ -1,36 +0,0 @@
package io.github.wulkanowy.services.sync.channels
import android.annotation.TargetApi
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import javax.inject.Inject
@TargetApi(26)
class NewAttendanceChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context
) : Channel {
companion object {
const val CHANNEL_ID = "new_attendance_channel"
}
override fun create() {
notificationManager.createNotificationChannel(
NotificationChannel(
CHANNEL_ID,
context.getString(R.string.channel_new_attendance),
NotificationManager.IMPORTANCE_HIGH
)
.apply {
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
})
}
}

View file

@ -1,36 +0,0 @@
package io.github.wulkanowy.services.sync.channels
import android.annotation.TargetApi
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import javax.inject.Inject
@TargetApi(26)
class TimetableChangeChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context
) : Channel {
companion object {
const val CHANNEL_ID = "change_timetable_channel"
}
override fun create() {
notificationManager.createNotificationChannel(
NotificationChannel(
CHANNEL_ID,
context.getString(R.string.channel_change_timetable),
NotificationManager.IMPORTANCE_HIGH
)
.apply {
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
})
}
}

View file

@ -1,180 +0,0 @@
package io.github.wulkanowy.services.sync.notifications
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Notification
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.data.repositories.NotificationRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.utils.PendingIntentCompat
import io.github.wulkanowy.utils.getCompatBitmap
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.nickOrName
import java.time.LocalDateTime
import javax.inject.Inject
import kotlin.random.Random
class AppNotificationManager @Inject constructor(
private val notificationManager: NotificationManagerCompat,
@ApplicationContext private val context: Context,
private val studentRepository: StudentRepository,
private val notificationRepository: NotificationRepository
) {
@SuppressLint("InlinedApi")
suspend fun sendSingleNotification(
notificationData: NotificationData,
notificationType: NotificationType,
student: Student
) {
val notification = NotificationCompat.Builder(context, notificationType.channel)
.setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary))
.setSmallIcon(R.drawable.ic_stat_all)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setContentIntent(
PendingIntent.getActivity(
context,
Random.nextInt(),
notificationData.intentToStart,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
)
.setContentTitle(notificationData.title)
.setContentText(notificationData.content)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(notificationData.content)
.also { builder ->
if (shouldShowStudentName()) {
builder.setSummaryText(student.nickOrName)
}
}
)
.build()
notificationManager.notify(Random.nextInt(), notification)
saveNotification(notificationData, notificationType, student)
}
@SuppressLint("InlinedApi")
suspend fun sendMultipleNotifications(
groupNotificationData: GroupNotificationData,
student: Student
) {
val notificationType = groupNotificationData.type
val groupType = notificationType.group ?: return
val group = "${groupType}_${student.id}"
sendSummaryNotification(groupNotificationData, group, student)
groupNotificationData.notificationDataList.forEach { notificationData ->
val notification = NotificationCompat.Builder(context, notificationType.channel)
.setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary))
.setSmallIcon(R.drawable.ic_stat_all)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setContentIntent(
PendingIntent.getActivity(
context,
Random.nextInt(),
notificationData.intentToStart,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
)
.setContentTitle(notificationData.title)
.setContentText(notificationData.content)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(notificationData.content)
.also { builder ->
if (shouldShowStudentName()) {
builder.setSummaryText(student.nickOrName)
}
}
)
.setGroup(group)
.build()
notificationManager.notify(Random.nextInt(), notification)
saveNotification(notificationData, groupNotificationData.type, student)
}
}
private suspend fun sendSummaryNotification(
groupNotificationData: GroupNotificationData,
group: String,
student: Student
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
val summaryNotification =
NotificationCompat.Builder(context, groupNotificationData.type.channel)
.setContentTitle(groupNotificationData.title)
.setContentText(groupNotificationData.content)
.setSmallIcon(groupNotificationData.type.icon)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setStyle(
NotificationCompat.InboxStyle()
.also { builder ->
if (shouldShowStudentName()) {
builder.setSummaryText(student.nickOrName)
}
groupNotificationData.notificationDataList.forEach {
builder.addLine(it.content)
}
}
)
.setContentIntent(
PendingIntent.getActivity(
context,
Random.nextInt(),
groupNotificationData.intentToStart,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
)
)
.setLocalOnly(true)
.setGroup(group)
.setGroupSummary(true)
.build()
val groupId = student.id * 100 + groupNotificationData.type.ordinal
notificationManager.notify(groupId.toInt(), summaryNotification)
}
private suspend fun saveNotification(
notificationData: NotificationData,
notificationType: NotificationType,
student: Student
) {
val notificationEntity = Notification(
studentId = student.id,
title = notificationData.title,
content = notificationData.content,
type = notificationType,
date = LocalDateTime.now()
)
notificationRepository.saveNotification(notificationEntity)
}
private suspend fun shouldShowStudentName(): Boolean =
studentRepository.getSavedStudents(decryptPass = false).size > 1
}

View file

@ -0,0 +1,102 @@
package io.github.wulkanowy.services.sync.notifications
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import androidx.annotation.PluralsRes
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.data.pojos.Notification
import io.github.wulkanowy.data.pojos.OneNotification
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.getCompatBitmap
import io.github.wulkanowy.utils.getCompatColor
import io.github.wulkanowy.utils.nickOrName
import kotlin.random.Random
abstract class BaseNotification(
private val context: Context,
private val notificationManager: NotificationManagerCompat,
) {
protected fun sendNotification(notification: Notification, student: Student) =
when (notification) {
is OneNotification -> sendOneNotification(notification, student)
is MultipleNotifications -> sendMultipleNotifications(notification, student)
}
private fun sendOneNotification(notification: OneNotification, student: Student?) {
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
getNotificationBuilder(notification).apply {
val content = context.getString(
notification.contentStringRes,
*notification.contentValues.toTypedArray()
)
setContentTitle(context.getString(notification.titleStringRes))
setContentText(content)
setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student?.nickOrName)
.bigText(content)
)
}.build()
)
}
private fun sendMultipleNotifications(notification: MultipleNotifications, student: Student) {
val group = notification.type.group + student.id
val groupId = student.id * 100 + notification.type.ordinal
notification.lines.forEach { item ->
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
getNotificationBuilder(notification).apply {
setContentTitle(getQuantityString(notification.titleStringRes, 1))
setContentText(item)
setStyle(
NotificationCompat.BigTextStyle()
.setSummaryText(student.nickOrName)
.bigText(item)
)
setGroup(group)
}.build()
)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
notificationManager.notify(
groupId.toInt(),
getNotificationBuilder(notification).apply {
setSmallIcon(notification.icon)
setGroup(group)
setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName))
setGroupSummary(true)
}.build()
)
}
private fun getNotificationBuilder(notification: Notification) = NotificationCompat
.Builder(context, notification.type.channel)
.setLargeIcon(context.getCompatBitmap(notification.icon, R.color.colorPrimary))
.setSmallIcon(R.drawable.ic_stat_all)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(
context, notification.startMenu.id,
MainActivity.getStartIntent(context, notification.startMenu, true),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
private fun getQuantityString(@PluralsRes id: Int, value: Int): String {
return context.resources.getQuantityString(id, value, value)
}
}

View file

@ -1,124 +0,0 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate
import java.time.LocalDateTime
import javax.inject.Inject
class ChangeTimetableNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context,
) {
suspend fun notify(items: List<Timetable>, student: Student) {
val currentTime = LocalDateTime.now()
val changedLessons = items.filter { (it.canceled || it.changes) && it.start > currentTime }
val notificationDataList = changedLessons.groupBy { it.date }
.map { (date, lessons) ->
getNotificationContents(date, lessons).map {
NotificationData(
title = context.getPlural(
R.plurals.timetable_notify_new_items_title,
1
),
content = it,
intentToStart = SplashActivity.getStartIntent(
context = context,
destination = Destination.Timetable(date)
)
)
}
}
.flatten()
.ifEmpty { return }
val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList,
title = context.getPlural(
R.plurals.timetable_notify_new_items_title,
changedLessons.size
),
content = context.getPlural(
R.plurals.timetable_notify_new_items_group,
changedLessons.size,
changedLessons.size
),
intentToStart = SplashActivity.getStartIntent(context, Destination.Timetable()),
type = NotificationType.CHANGE_TIMETABLE
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
}
private fun getNotificationContents(date: LocalDate, lessons: List<Timetable>): List<String> {
val formattedDate = date.toFormattedString("EEE dd.MM")
return if (lessons.size > 2) {
listOf(
context.getPlural(
R.plurals.timetable_notify_new_items,
lessons.size,
formattedDate,
lessons.size,
)
)
} else {
lessons.map {
buildString {
append(
context.getString(
R.string.timetable_notify_lesson,
formattedDate,
it.number,
it.subject
)
)
if (it.roomOld.isNotBlank()) {
appendLine()
append(
context.getString(
R.string.timetable_notify_change_room,
it.roomOld,
it.room
)
)
}
if (it.teacherOld.isNotBlank() && it.teacher != it.teacherOld) {
appendLine()
append(
context.getString(
R.string.timetable_notify_change_teacher,
it.teacherOld,
it.teacher
)
)
}
if (it.subjectOld.isNotBlank()) {
appendLine()
append(
context.getString(
R.string.timetable_notify_change_subject,
it.subjectOld,
it.subject
)
)
}
if (it.info.isNotBlank()) {
appendLine()
append(it.info)
}
}
}
}
}
}

View file

@ -1,55 +0,0 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.descriptionRes
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
class NewAttendanceNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) {
suspend fun notify(items: List<Attendance>, student: Student) {
val lines = items.filterNot { it.presence || it.name == "UNKNOWN" }
.map {
val description = context.getString(it.descriptionRes)
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: $description"
}
.ifEmpty { return }
val notificationDataList = lines.map {
NotificationData(
title = context.getPlural(R.plurals.attendance_notify_new_items_title, 1),
content = it,
intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance)
)
}
val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList,
title = context.getPlural(
R.plurals.attendance_notify_new_items_title,
notificationDataList.size
),
content = context.getPlural(
R.plurals.attendance_notify_new_items,
notificationDataList.size,
notificationDataList.size
),
intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance),
type = NotificationType.NEW_ATTENDANCE
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
}
}

View file

@ -1,52 +1,38 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDateTime
import javax.inject.Inject
class NewConferenceNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) {
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
suspend fun notify(items: List<Conference>, student: Student) {
fun notify(items: List<Conference>, student: Student) {
val today = LocalDateTime.now()
val lines = items.filter { !it.date.isBefore(today) }
.map {
val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}"
}
.ifEmpty { return }
}.ifEmpty { return }
val notificationDataList = lines.map {
NotificationData(
title = context.getPlural(R.plurals.conference_notify_new_item_title, 1),
content = it,
intentToStart = SplashActivity.getStartIntent(context, Destination.Conference)
)
}
val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList,
title = context.getPlural(R.plurals.conference_notify_new_item_title, lines.size),
content = context.getPlural(
R.plurals.conference_notify_new_items,
lines.size,
lines.size
),
intentToStart = SplashActivity.getStartIntent(context, Destination.Conference),
type = NotificationType.NEW_CONFERENCE
val notification = MultipleNotifications(
type = NotificationType.NEW_CONFERENCE,
icon = R.drawable.ic_more_conferences,
titleStringRes = R.plurals.conference_notify_new_item_title,
contentStringRes = R.plurals.conference_notify_new_items,
summaryStringRes = R.plurals.conference_number_item,
startMenu = MainView.Section.CONFERENCE,
lines = lines
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
sendNotification(notification, student)
}
}

View file

@ -1,52 +1,38 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate
import javax.inject.Inject
class NewExamNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) {
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
suspend fun notify(items: List<Exam>, student: Student) {
fun notify(items: List<Exam>, student: Student) {
val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) }
.map {
val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}"
}
.ifEmpty { return }
}.ifEmpty { return }
val notificationDataList = lines.map {
NotificationData(
title = context.getPlural(R.plurals.exam_notify_new_item_title, 1),
content = it,
intentToStart = SplashActivity.getStartIntent(context, Destination.Exam),
)
}
val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList,
title = context.getPlural(R.plurals.exam_notify_new_item_title, lines.size),
content = context.getPlural(
R.plurals.exam_notify_new_item_content,
lines.size,
lines.size
),
intentToStart = SplashActivity.getStartIntent(context, Destination.Exam),
type = NotificationType.NEW_EXAM
val notification = MultipleNotifications(
type = NotificationType.NEW_EXAM,
icon = R.drawable.ic_main_exam,
titleStringRes = R.plurals.exam_notify_new_item_title,
contentStringRes = R.plurals.exam_notify_new_item_content,
summaryStringRes = R.plurals.exam_number_item,
startMenu = MainView.Section.EXAM,
lines = lines
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
sendNotification(notification, student)
}
}

View file

@ -1,88 +1,66 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class NewGradeNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) {
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
suspend fun notifyDetails(items: List<Grade>, student: Student) {
val notificationDataList = items.map {
NotificationData(
title = context.getPlural(R.plurals.grade_new_items, 1),
content = "${it.subject}: ${it.entry}",
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade),
)
fun notifyDetails(items: List<Grade>, student: Student) {
val notification = MultipleNotifications(
type = NotificationType.NEW_GRADE_DETAILS,
icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items,
contentStringRes = R.plurals.grade_notify_new_items,
summaryStringRes = R.plurals.grade_number_item,
startMenu = MainView.Section.GRADE,
lines = items.map {
"${it.subject}: ${it.entry}"
}
val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList,
title = context.getPlural(R.plurals.grade_new_items, items.size),
content = context.getPlural(R.plurals.grade_notify_new_items, items.size, items.size),
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade),
type = NotificationType.NEW_GRADE_DETAILS
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
sendNotification(notification, student)
}
suspend fun notifyPredicted(items: List<GradeSummary>, student: Student) {
val notificationDataList = items.map {
NotificationData(
title = context.getPlural(R.plurals.grade_new_items_predicted, 1),
content = "${it.subject}: ${it.predictedGrade}",
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade),
)
fun notifyPredicted(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotifications(
type = NotificationType.NEW_GRADE_PREDICTED,
icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items_predicted,
contentStringRes = R.plurals.grade_notify_new_items_predicted,
summaryStringRes = R.plurals.grade_number_item,
startMenu = MainView.Section.GRADE,
lines = items.map {
"${it.subject}: ${it.predictedGrade}"
}
val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList,
title = context.getPlural(R.plurals.grade_new_items_predicted, items.size),
content = context.getPlural(
R.plurals.grade_notify_new_items_predicted,
items.size,
items.size
),
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade),
type = NotificationType.NEW_GRADE_PREDICTED
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
sendNotification(notification, student)
}
suspend fun notifyFinal(items: List<GradeSummary>, student: Student) {
val notificationDataList = items.map {
NotificationData(
title = context.getPlural(R.plurals.grade_new_items_final, 1),
content = "${it.subject}: ${it.finalGrade}",
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade),
)
fun notifyFinal(items: List<GradeSummary>, student: Student) {
val notification = MultipleNotifications(
type = NotificationType.NEW_GRADE_FINAL,
icon = R.drawable.ic_stat_grade,
titleStringRes = R.plurals.grade_new_items_final,
contentStringRes = R.plurals.grade_notify_new_items_final,
summaryStringRes = R.plurals.grade_number_item,
startMenu = MainView.Section.GRADE,
lines = items.map {
"${it.subject}: ${it.finalGrade}"
}
val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList,
title = context.getPlural(R.plurals.grade_new_items_final, items.size),
content = context.getPlural(
R.plurals.grade_notify_new_items_final,
items.size,
items.size
),
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade),
type = NotificationType.NEW_GRADE_FINAL
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
sendNotification(notification, student)
}
}

View file

@ -1,52 +1,38 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate
import javax.inject.Inject
class NewHomeworkNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) {
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
suspend fun notify(items: List<Homework>, student: Student) {
fun notify(items: List<Homework>, student: Student) {
val today = LocalDate.now()
val lines = items.filter { !it.date.isBefore(today) }
.map {
val lines = items.filter { !it.date.isBefore(today) }.map {
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}"
}
.ifEmpty { return }
}.ifEmpty { return }
val notificationDataList = lines.map {
NotificationData(
title = context.getPlural(R.plurals.homework_notify_new_item_title, 1),
content = it,
intentToStart = SplashActivity.getStartIntent(context, Destination.Homework),
)
}
val groupNotificationData = GroupNotificationData(
title = context.getPlural(R.plurals.homework_notify_new_item_title, lines.size),
content = context.getPlural(
R.plurals.homework_notify_new_item_content,
lines.size,
lines.size
),
intentToStart = SplashActivity.getStartIntent(context, Destination.Homework),
val notification = MultipleNotifications(
type = NotificationType.NEW_HOMEWORK,
notificationDataList = notificationDataList
icon = R.drawable.ic_more_homework,
titleStringRes = R.plurals.homework_notify_new_item_title,
contentStringRes = R.plurals.homework_notify_new_item_content,
summaryStringRes = R.plurals.homework_number_item,
startMenu = MainView.Section.HOMEWORK,
lines = lines
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
sendNotification(notification, student)
}
}

View file

@ -1,34 +1,30 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.data.pojos.OneNotification
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class NewLuckyNumberNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) {
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
suspend fun notify(item: LuckyNumber, student: Student) {
val notificationData = NotificationData(
title = context.getString(R.string.lucky_number_notify_new_item_title),
content = context.getString(
R.string.lucky_number_notify_new_item,
item.luckyNumber.toString()
),
intentToStart = SplashActivity.getStartIntent(context, Destination.LuckyNumber)
fun notify(item: LuckyNumber, student: Student) {
val notification = OneNotification(
type = NotificationType.NEW_LUCKY_NUMBER,
icon = R.drawable.ic_stat_luckynumber,
titleStringRes = R.string.lucky_number_notify_new_item_title,
contentStringRes = R.string.lucky_number_notify_new_item,
startMenu = MainView.Section.LUCKY_NUMBER,
contentValues = listOf(item.luckyNumber.toString())
)
appNotificationManager.sendSingleNotification(
notificationData = notificationData,
notificationType = NotificationType.NEW_LUCKY_NUMBER,
student = student
)
sendNotification(notification, student)
}
}

View file

@ -1,39 +1,33 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class NewMessageNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) {
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
suspend fun notify(items: List<Message>, student: Student) {
val notificationDataList = items.map {
NotificationData(
title = context.getPlural(R.plurals.message_new_items, 1),
content = "${it.sender}: ${it.subject}",
intentToStart = SplashActivity.getStartIntent(context, Destination.Message),
)
fun notify(items: List<Message>, student: Student) {
val notification = MultipleNotifications(
type = NotificationType.NEW_MESSAGE,
icon = R.drawable.ic_stat_message,
titleStringRes = R.plurals.message_new_items,
contentStringRes = R.plurals.message_notify_new_items,
summaryStringRes = R.plurals.message_number_item,
startMenu = MainView.Section.MESSAGE,
lines = items.map {
"${it.sender}: ${it.subject}"
}
val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList,
title = context.getPlural(R.plurals.message_new_items, items.size),
content = context.getPlural(R.plurals.message_notify_new_items, items.size, items.size),
intentToStart = SplashActivity.getStartIntent(context, Destination.Message),
type = NotificationType.NEW_MESSAGE
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
sendNotification(notification, student)
}
}

View file

@ -1,46 +1,46 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class NewNoteNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) {
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
suspend fun notify(items: List<Note>, student: Student) {
val notificationDataList = items.map {
val titleRes = when (NoteCategory.getByValue(it.categoryType)) {
fun notify(items: List<Note>, student: Student) {
val notification = MultipleNotifications(
type = NotificationType.NEW_NOTE,
icon = R.drawable.ic_stat_note,
titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
NoteCategory.POSITIVE -> R.plurals.praise_new_items
NoteCategory.NEUTRAL -> R.plurals.neutral_note_new_items
else -> R.plurals.note_new_items
},
contentStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
NoteCategory.POSITIVE -> R.plurals.praise_notify_new_items
NoteCategory.NEUTRAL -> R.plurals.neutral_note_notify_new_items
else -> R.plurals.note_notify_new_items
},
summaryStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
NoteCategory.POSITIVE -> R.plurals.praise_number_item
NoteCategory.NEUTRAL -> R.plurals.neutral_note_number_item
else -> R.plurals.note_number_item
},
startMenu = MainView.Section.NOTE,
lines = items.map {
"${it.teacher}: ${it.category}"
}
NotificationData(
title = context.getPlural(titleRes, 1),
content = "${it.teacher}: ${it.category}",
intentToStart = SplashActivity.getStartIntent(context, Destination.Note),
)
}
val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList,
intentToStart = SplashActivity.getStartIntent(context, Destination.Note),
title = context.getPlural(R.plurals.note_new_items, items.size),
content = context.getPlural(R.plurals.note_notify_new_items, items.size, items.size),
type = NotificationType.NEW_NOTE
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
sendNotification(notification, student)
}
}

View file

@ -1,54 +1,33 @@
package io.github.wulkanowy.services.sync.notifications
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.GroupNotificationData
import io.github.wulkanowy.data.pojos.NotificationData
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.data.pojos.MultipleNotifications
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class NewSchoolAnnouncementNotification @Inject constructor(
private val appNotificationManager: AppNotificationManager,
@ApplicationContext private val context: Context
) {
@ApplicationContext private val context: Context,
notificationManager: NotificationManagerCompat,
) : BaseNotification(context, notificationManager) {
suspend fun notify(items: List<SchoolAnnouncement>, student: Student) {
val notificationDataList = items.map {
NotificationData(
intentToStart = SplashActivity.getStartIntent(
context = context,
destination = Destination.SchoolAnnouncement
),
title = context.getPlural(
R.plurals.school_announcement_notify_new_item_title,
1
),
content = "${it.subject}: ${it.content}"
)
}
val groupNotificationData = GroupNotificationData(
fun notify(items: List<SchoolAnnouncement>, student: Student) {
val notification = MultipleNotifications(
type = NotificationType.NEW_ANNOUNCEMENT,
intentToStart = SplashActivity.getStartIntent(
context = context,
destination = Destination.SchoolAnnouncement
),
title = context.getPlural(
R.plurals.school_announcement_notify_new_item_title,
items.size
),
content = context.getPlural(
R.plurals.school_announcement_notify_new_items,
items.size,
items.size
),
notificationDataList = notificationDataList
icon = R.drawable.ic_all_about,
titleStringRes = R.plurals.school_announcement_notify_new_item_title,
contentStringRes = R.plurals.school_announcement_notify_new_items,
summaryStringRes = R.plurals.school_announcement_number_item,
startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT,
lines = items.map {
"${it.subject}: ${it.content}"
}
)
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
sendNotification(notification, student)
}
}

View file

@ -1,8 +1,6 @@
package io.github.wulkanowy.services.sync.notifications
import io.github.wulkanowy.R
import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel
import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel
import io.github.wulkanowy.services.sync.channels.NewConferencesChannel
import io.github.wulkanowy.services.sync.channels.NewExamChannel
import io.github.wulkanowy.services.sync.channels.NewGradesChannel
@ -10,77 +8,16 @@ import io.github.wulkanowy.services.sync.channels.NewHomeworkChannel
import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel
import io.github.wulkanowy.services.sync.channels.PushChannel
import io.github.wulkanowy.services.sync.channels.TimetableChangeChannel
enum class NotificationType(
val group: String?,
val channel: String,
val icon: Int
) {
NEW_CONFERENCE(
group = "new_conferences_group",
channel = NewConferencesChannel.CHANNEL_ID,
icon = R.drawable.ic_more_conferences,
),
NEW_EXAM(
group = "new_exam_group",
channel = NewExamChannel.CHANNEL_ID,
icon = R.drawable.ic_main_exam
),
NEW_GRADE_DETAILS(
group = "new_grade_details_group",
channel = NewGradesChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_grade,
),
NEW_GRADE_PREDICTED(
group = "new_grade_predicted_group",
channel = NewGradesChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_grade,
),
NEW_GRADE_FINAL(
group = "new_grade_final_group",
channel = NewGradesChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_grade,
),
NEW_HOMEWORK(
group = "new_homework_group",
channel = NewHomeworkChannel.CHANNEL_ID,
icon = R.drawable.ic_more_homework,
),
NEW_LUCKY_NUMBER(
group = null,
channel = LuckyNumberChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_luckynumber,
),
NEW_MESSAGE(
group = "new_message_group",
channel = NewMessagesChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_message,
),
NEW_NOTE(
group = "new_notes_group",
channel = NewNotesChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_note
),
NEW_ANNOUNCEMENT(
group = "new_school_announcements_group",
channel = NewSchoolAnnouncementsChannel.CHANNEL_ID,
icon = R.drawable.ic_all_about
),
CHANGE_TIMETABLE(
group = "change_timetable_group",
channel = TimetableChangeChannel.CHANNEL_ID,
icon = R.drawable.ic_main_timetable
),
NEW_ATTENDANCE(
group = "new_attendance_group",
channel = NewAttendanceChannel.CHANNEL_ID,
icon = R.drawable.ic_main_attendance
),
PUSH(
group = null,
channel = PushChannel.CHANNEL_ID,
icon = R.drawable.ic_stat_all
)
enum class NotificationType(val group: String, val channel: String) {
NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID),
NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID),
NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID),
NEW_GRADE_PREDICTED("new_grade_predicted_group", NewGradesChannel.CHANNEL_ID),
NEW_GRADE_FINAL("new_grade_final_group", NewGradesChannel.CHANNEL_ID),
NEW_HOMEWORK("new_homework_group", NewHomeworkChannel.CHANNEL_ID),
NEW_LUCKY_NUMBER("lucky_number_group", LuckyNumberChannel.CHANNEL_ID),
NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID),
NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID),
NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID),
}

View file

@ -3,40 +3,15 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification
import io.github.wulkanowy.utils.previousOrSameSchoolDay
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now
import javax.inject.Inject
class AttendanceWork @Inject constructor(
private val attendanceRepository: AttendanceRepository,
private val newAttendanceNotification: NewAttendanceNotification,
private val preferencesRepository: PreferencesRepository
) : Work {
class AttendanceWork @Inject constructor(private val attendanceRepository: AttendanceRepository) : Work {
override suspend fun doWork(student: Student, semester: Semester) {
attendanceRepository.getAttendance(
student = student,
semester = semester,
start = now().previousOrSameSchoolDay,
end = now().previousOrSameSchoolDay,
forceRefresh = true,
notify = preferencesRepository.isNotificationsEnable
)
.waitForResult()
attendanceRepository.getAttendanceFromDatabase(semester, now().minusDays(7), now())
.first()
.filterNot { it.isNotified }
.let {
if (it.isNotEmpty()) newAttendanceNotification.notify(it, student)
attendanceRepository.updateTimetable(it.onEach { attendance ->
attendance.isNotified = true
})
}
attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true).waitForResult()
}
}

View file

@ -5,7 +5,8 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.HomeworkRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.sync.notifications.NewHomeworkNotification
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now
@ -21,13 +22,13 @@ class HomeworkWork @Inject constructor(
homeworkRepository.getHomework(
student = student,
semester = semester,
start = now().nextOrSameSchoolDay,
end = now().nextOrSameSchoolDay,
start = now().monday,
end = now().sunday,
forceRefresh = true,
notify = preferencesRepository.isNotificationsEnable
).waitForResult()
homeworkRepository.getHomeworkFromDatabase(semester, now(), now().plusDays(7)).first()
homeworkRepository.getHomeworkFromDatabase(semester, now().monday, now().sunday).first()
.filter { !it.isNotified }.let {
if (it.isNotEmpty()) newHomeworkNotification.notify(it, student)

View file

@ -2,41 +2,18 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.waitForResult
import kotlinx.coroutines.flow.first
import java.time.LocalDate.now
import javax.inject.Inject
class TimetableWork @Inject constructor(
private val timetableRepository: TimetableRepository,
private val changeTimetableNotification: ChangeTimetableNotification,
private val preferencesRepository: PreferencesRepository
private val timetableRepository: TimetableRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester) {
timetableRepository.getTimetable(
student = student,
semester = semester,
start = now().nextOrSameSchoolDay,
end = now().nextOrSameSchoolDay,
forceRefresh = true,
notify = preferencesRepository.isNotificationsEnable
)
.waitForResult()
timetableRepository.getTimetableFromDatabase(semester, now(), now().plusDays(7))
.first()
.filterNot { it.isNotified }
.let {
if (it.isNotEmpty()) changeTimetableNotification.notify(it, student)
timetableRepository.updateTimetable(it.onEach { timetable ->
timetable.isNotified = true
})
}
timetableRepository.getTimetable(student, semester, now().monday, now().sunday, true).waitForResult()
}
}

View file

@ -1,11 +1,14 @@
package io.github.wulkanowy.ui.base
import android.app.ActivityManager
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.viewbinding.ViewBinding
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
@ -37,6 +40,7 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
themeManager.applyActivityTheme(this)
super.onCreate(savedInstanceState)
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
@Suppress("DEPRECATION")
setTaskDescription(
@ -79,8 +83,8 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
}
override fun openClearLoginView() {
startActivity(LoginActivity.getStartIntent(this))
finishAffinity()
startActivity(LoginActivity.getStartIntent(this)
.apply { addFlags(FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK) })
}
override fun onDestroy() {

View file

@ -2,33 +2,32 @@ package io.github.wulkanowy.ui.base
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import androidx.fragment.app.FragmentPagerAdapter
class BaseFragmentPagerAdapter(
private val fragmentManager: FragmentManager,
private val pagesCount: Int,
lifecycle: Lifecycle,
) : FragmentStateAdapter(fragmentManager, lifecycle), TabLayoutMediator.TabConfigurationStrategy {
//TODO Use ViewPager2
class BaseFragmentPagerAdapter(private val fragmentManager: FragmentManager) :
FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
lateinit var itemFactory: (position: Int) -> Fragment
var titleFactory: (position: Int) -> String? = { "" }
private val pages = mutableMapOf<Fragment, String?>()
var containerId = 0
fun getFragmentInstance(position: Int): Fragment? {
require(containerId != 0) { "Container id is 0" }
return fragmentManager.findFragmentByTag("f$position")
return fragmentManager.findFragmentByTag("android:switcher:$containerId:$position")
}
override fun createFragment(position: Int): Fragment = itemFactory(position)
override fun getItemCount() = pagesCount
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
tab.text = titleFactory(position)
fun addFragments(fragments: List<Fragment>) {
fragments.forEach { pages[it] = null }
}
fun addFragmentsWithTitle(pages: Map<Fragment, String>) {
this.pages.putAll(pages)
}
override fun getItem(position: Int) = pages.keys.elementAt(position)
override fun getCount() = pages.size
override fun getPageTitle(position: Int) = pages.values.elementAt(position)
}

View file

@ -6,27 +6,29 @@ import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import kotlin.coroutines.CoroutineContext
open class BasePresenter<T : BaseView>(
protected val errorHandler: ErrorHandler,
protected val studentRepository: StudentRepository
) {
private val job = SupervisorJob()
) : CoroutineScope {
protected val presenterScope = CoroutineScope(job + Dispatchers.Main)
private var job = Job()
private val childrenJobs = mutableMapOf<String, Job>()
private val jobs = mutableMapOf<String, Job>()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
var view: T? = null
open fun onAttachView(view: T) {
job = Job()
this.view = view
errorHandler.apply {
showErrorMessage = view::showError
@ -62,22 +64,22 @@ open class BasePresenter<T : BaseView>(
}
fun <T> Flow<T>.launch(individualJobTag: String = "load"): Job {
childrenJobs[individualJobTag]?.cancel()
val job = catch { errorHandler.dispatch(it) }.launchIn(presenterScope)
childrenJobs[individualJobTag] = job
jobs[individualJobTag]?.cancel()
val job = catch { errorHandler.dispatch(it) }.launchIn(this@BasePresenter)
jobs[individualJobTag] = job
Timber.d("Job $individualJobTag launched in ${this@BasePresenter.javaClass.simpleName}: $job")
return job
}
fun cancelJobs(vararg names: String) {
names.forEach {
childrenJobs[it]?.cancel()
jobs[it]?.cancel()
}
}
open fun onDetachView() {
job.cancelChildren()
errorHandler.clear()
view = null
job.cancel()
errorHandler.clear()
}
}

View file

@ -11,7 +11,6 @@ import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService
import androidx.core.view.isGone
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogErrorBinding
@ -25,6 +24,8 @@ import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import okhttp3.internal.http2.StreamResetException
import java.io.InterruptedIOException
import java.io.PrintWriter
import java.io.StringWriter
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
@ -63,26 +64,26 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val errorStacktrace = error.stackTraceToString()
val stringWriter = StringWriter().apply {
error.printStackTrace(PrintWriter(this))
}
with(binding) {
errorDialogContent.text = errorStacktrace.replace(": ${error.localizedMessage}", "")
errorDialogContent.text = stringWriter.toString()
with(errorDialogHorizontalScroll) {
post { fullScroll(HorizontalScrollView.FOCUS_LEFT) }
}
errorDialogCopy.setOnClickListener {
val clip = ClipData.newPlainText("Error details", errorStacktrace)
val clip = ClipData.newPlainText("wulkanowy", stringWriter.toString())
activity?.getSystemService<ClipboardManager>()?.setPrimaryClip(clip)
Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show()
}
errorDialogCancel.setOnClickListener { dismiss() }
errorDialogReport.setOnClickListener {
openConfirmDialog { openEmailClient(errorStacktrace) }
openConfirmDialog { openEmailClient(stringWriter.toString()) }
}
errorDialogHumanizedMessage.text = resources.getString(error)
errorDialogErrorMessage.text = error.localizedMessage
errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank()
errorDialogMessage.text = resources.getString(error)
errorDialogReport.isEnabled = when (error) {
is UnknownHostException,
is InterruptedIOException,

View file

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

View file

@ -41,15 +41,14 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
)
}
private fun isThemeApplicable(activity: AppCompatActivity) =
activity.packageManager
private fun isThemeApplicable(activity: AppCompatActivity): Boolean {
return activity.packageManager
.getPackageInfo(activity.packageName, GET_ACTIVITIES)
.activities
.singleOrNull { it.name == activity::class.java.canonicalName }
?.theme
.let {
.activities.singleOrNull { it.name == activity::class.java.canonicalName }
?.theme.let {
it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar
|| it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black
|| it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black
}
}
}

View file

@ -1,136 +0,0 @@
package io.github.wulkanowy.ui.modules
import androidx.fragment.app.Fragment
import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import java.io.Serializable
import java.time.LocalDate
sealed interface Destination : Serializable {
/*
Type in children classes have to be as getter to avoid null in enums
https://stackoverflow.com/questions/68866453/kotlin-enum-val-is-returning-null-despite-being-set-at-compile-time
*/
val type: Type
val fragment: Fragment
enum class Type(val defaultDestination: Destination) {
DASHBOARD(Dashboard),
GRADE(Grade),
ATTENDANCE(Attendance),
EXAM(Exam),
TIMETABLE(Timetable()),
HOMEWORK(Homework),
NOTE(Note),
CONFERENCE(Conference),
SCHOOL_ANNOUNCEMENT(SchoolAnnouncement),
SCHOOL(School),
LUCKY_NUMBER(More),
MORE(More),
MESSAGE(Message);
}
object Dashboard : Destination {
override val type get() = Type.DASHBOARD
override val fragment get() = DashboardFragment.newInstance()
}
object Grade : Destination {
override val type get() = Type.GRADE
override val fragment get() = GradeFragment.newInstance()
}
object Attendance : Destination {
override val type get() = Type.ATTENDANCE
override val fragment get() = AttendanceFragment.newInstance()
}
object Exam : Destination {
override val type get() = Type.EXAM
override val fragment get() = ExamFragment.newInstance()
}
data class Timetable(val date: LocalDate? = null) : Destination {
override val type get() = Type.TIMETABLE
override val fragment get() = TimetableFragment.newInstance(date)
}
object Homework : Destination {
override val type get() = Type.HOMEWORK
override val fragment get() = HomeworkFragment.newInstance()
}
object Note : Destination {
override val type get() = Type.NOTE
override val fragment get() = NoteFragment.newInstance()
}
object Conference : Destination {
override val type get() = Type.CONFERENCE
override val fragment get() = ConferenceFragment.newInstance()
}
object SchoolAnnouncement : Destination {
override val type get() = Type.SCHOOL_ANNOUNCEMENT
override val fragment get() = SchoolAnnouncementFragment.newInstance()
}
object School : Destination {
override val type get() = Type.SCHOOL
override val fragment get() = SchoolFragment.newInstance()
}
object LuckyNumber : Destination {
override val type get() = Type.LUCKY_NUMBER
override val fragment get() = LuckyNumberFragment.newInstance()
}
object More : Destination {
override val type get() = Type.MORE
override val fragment get() = MoreFragment.newInstance()
}
object Message : Destination {
override val type get() = Type.MESSAGE
override val fragment get() = MessageFragment.newInstance()
}
}

View file

@ -82,8 +82,7 @@ class AboutPresenter @Inject constructor(
private fun loadData() {
view?.run {
updateData(
listOfNotNull(
updateData(listOfNotNull(
versionRes,
creatorsRes,
feedbackRes,
@ -94,8 +93,7 @@ class AboutPresenter @Inject constructor(
homepageRes,
licensesRes,
privacyRes
)
)
))
}
}
}

View file

@ -31,7 +31,7 @@ class LicensePresenter @Inject constructor(
private fun loadData() {
flowWithResource {
withContext(dispatchers.io) {
withContext(dispatchers.backgroundThread) {
view?.appLibraries.orEmpty()
}
}.onEach {

View file

@ -121,14 +121,10 @@ class AccountDetailsFragment :
}
}
override fun popViewToMain() {
override fun popView() {
(requireActivity() as MainActivity).popView(2)
}
override fun popViewToAccounts() {
(requireActivity() as MainActivity).popView(1)
}
override fun recreateMainView() {
requireActivity().recreate()
}

View file

@ -119,7 +119,7 @@ class AccountDetailsPresenter @Inject constructor(
}
}
}.afterLoading {
view?.popViewToMain()
view?.popView()
}.launch("switch")
}
@ -152,14 +152,11 @@ class AccountDetailsPresenter @Inject constructor(
syncManager.stopSyncWorker()
openClearLoginView()
}
studentWithSemesters?.student?.isCurrent == true -> {
studentWithSemesters!!.student.isCurrent -> {
Timber.i("Logout result: Logout student and switch to another")
recreateMainView()
}
else -> {
Timber.i("Logout result: Logout student")
recreateMainView()
}
else -> Timber.i("Logout result: Logout student")
}
}
Status.ERROR -> {
@ -168,11 +165,7 @@ class AccountDetailsPresenter @Inject constructor(
}
}
}.afterLoading {
if (studentWithSemesters?.student?.isCurrent == true) {
view?.popViewToMain()
} else {
view?.popViewToAccounts()
}
view?.popView()
}.launch("logout")
}

View file

@ -15,9 +15,7 @@ interface AccountDetailsView : BaseView {
fun showLogoutConfirmDialog()
fun popViewToMain()
fun popViewToAccounts()
fun popView()
fun recreateMainView()

View file

@ -3,9 +3,12 @@ package io.github.wulkanowy.ui.modules.account.accountedit
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.StateListDrawable
import android.os.Build
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
@ -49,12 +52,29 @@ class AccountEditColorAdapter @Inject constructor() :
}
}
private fun Int.createForegroundDrawable(): Drawable {
private fun Int.createForegroundDrawable(): Drawable =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val mask = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(Color.BLACK)
}
return RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask)
RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask)
} else {
val foreground = StateListDrawable().apply {
alpha = 80
setEnterFadeDuration(250)
setExitFadeDuration(250)
}
val mask = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(this@createForegroundDrawable.rippleColor)
}
foreground.apply {
addState(intArrayOf(android.R.attr.state_pressed), mask)
addState(intArrayOf(), ColorDrawable(Color.TRANSPARENT))
}
}
private inline val Int.rippleColor: Int

View file

@ -9,7 +9,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.enums.SentExcuseStatus
import io.github.wulkanowy.databinding.ItemAttendanceBinding
import io.github.wulkanowy.utils.descriptionRes
import io.github.wulkanowy.utils.description
import io.github.wulkanowy.utils.isExcusableOrNotExcused
import javax.inject.Inject
@ -36,7 +36,7 @@ class AttendanceAdapter @Inject constructor() :
with(holder.binding) {
attendanceItemNumber.text = item.number.toString()
attendanceItemSubject.text = item.subject
attendanceItemDescription.setText(item.descriptionRes)
attendanceItemDescription.setText(item.description)
attendanceItemAlert.visibility = item.run { if (absence && !excused) View.VISIBLE else View.INVISIBLE }
attendanceItemNumber.visibility = View.GONE
attendanceItemExcuseInfo.visibility = View.GONE
@ -46,7 +46,7 @@ class AttendanceAdapter @Inject constructor() :
onExcuseCheckboxSelect(item, checked)
}
when (item.excuseStatus?.let { SentExcuseStatus.valueOf(it)}) {
when (if (item.excuseStatus != null) SentExcuseStatus.valueOf(item.excuseStatus) else null) {
SentExcuseStatus.WAITING -> {
attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_waiting)
attendanceItemExcuseInfo.visibility = View.VISIBLE

Some files were not shown because too many files have changed in this diff Show more