diff --git a/.circleci/config.yml b/.circleci/config.yml index f1f6a4c15..789123fa4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -38,7 +38,7 @@ jobs: command: ./gradlew build -x test -x lint -x fabricGenerateResourcesRelease -x packageRelease --no-daemon --stacktrace --console=plain -PdisablePreDex - run: name: Run FOSSA - command: fossa --no-ansi + command: fossa --no-ansi || true - persist_to_workspace: root: *workspace_root paths: @@ -113,7 +113,7 @@ jobs: adb shell input keyevent 82 - run: name: Run instrumented tests - command: ./gradlew clean createDebugCoverageReport jacocoTestReport --no-daemon --stacktrace --console=plain -PdisablePreDex + command: ./gradlew clean createDebugCoverageReport jacocoTestReport --no-daemon --stacktrace --console=plain -PdisablePreDex -PdisableCrashlytics - run: name: Collect logs from emulator command: adb logcat -d > ./app/build/reports/logcat_emulator.txt diff --git a/.gitignore b/.gitignore index 8ad04ebf0..3eb9aa654 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,6 @@ local.properties .idea/vcs.xml .idea/workspace.xml .idea/caches/ -.idea/codeStyles/ *.iml # OS-specific files @@ -44,7 +43,7 @@ local.properties .Trashes ehthumbs.db Thumbs.db -.idea/codeStyles/ .idea/caches/ ./app/key.p12 ./app/upload-key.jks +*.log diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..89f657f27 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,174 @@ + + + + diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..0f7bc519d --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/build.gradle b/app/build.gradle index c8d99fa22..b7f5face2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,6 +7,8 @@ apply plugin: 'com.github.triplet.play' apply from: 'jacoco.gradle' apply from: 'sonarqube.gradle' +def fabricApiKey = System.getenv("FABRIC_API_KEY") ?: "null" + android { compileSdkVersion 28 buildToolsVersion '28.0.3' @@ -29,9 +31,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true playAccountConfig = playAccountConfigs.defaultAccountConfig - manifestPlaceholders = [ - fabricApiKey: System.getenv("FABRIC_API_KEY") ?: "null" - ] + manifestPlaceholders = [ fabricApiKey: fabricApiKey ] } signingConfigs { @@ -45,18 +45,24 @@ android { buildTypes { release { + buildConfigField "boolean", "FABRIC_ENABLED", "true" minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } debug { + buildConfigField "boolean", "FABRIC_ENABLED", fabricApiKey == "null" ? "false" : "true" applicationIdSuffix ".dev" versionNameSuffix "-dev" testCoverageEnabled = true - ext.enableCrashlytics = false + ext.enableCrashlytics = fabricApiKey != "null" && !project.hasProperty("disableCrashlytics") multiDexKeepProguard file('proguard-multidex-rules.pro') } } + + lintOptions { + disable 'HardwareIds' + } } androidExtensions { @@ -72,7 +78,7 @@ ext.androidx_version = "1.0.0" dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation('com.github.wulkanowy:api:1400211f0d') { exclude module: "threetenbp" } + implementation('com.github.wulkanowy:api:e829b094de') { exclude module: "threetenbp" } implementation "androidx.legacy:legacy-support-v4:$androidx_version" implementation "androidx.appcompat:appcompat:$androidx_version" @@ -89,12 +95,13 @@ dependencies { kapt "com.google.dagger:dagger-compiler:2.16" kapt "com.google.dagger:dagger-android-processor:2.16" - implementation "androidx.room:room-runtime:2.1.0-alpha01" - implementation "androidx.room:room-rxjava2:2.1.0-alpha01" - kapt "androidx.room:room-compiler:2.1.0-alpha01" + implementation "androidx.room:room-runtime:2.1.0-alpha02" + implementation "androidx.room:room-rxjava2:2.1.0-alpha02" + kapt "androidx.room:room-compiler:2.1.0-alpha02" implementation "eu.davidea:flexible-adapter:5.1.0" implementation "eu.davidea:flexible-adapter-ui:1.0.0" + implementation "com.aurelhubert:ahbottomnavigation:2.2.0" implementation 'com.ncapdevi:frag-nav:3.0.0-RC3' @@ -120,9 +127,10 @@ dependencies { testImplementation "org.mockito:mockito-inline:2.23.0" testImplementation 'org.threeten:threetenbp:1.3.7' - androidTestImplementation 'androidx.test:core:1.0.0-beta02' - androidTestImplementation 'androidx.test:runner:1.1.0-beta02' - androidTestImplementation 'androidx.test.ext:junit:1.0.0-beta02' + androidTestImplementation 'androidx.test:core:1.0.0' + androidTestImplementation 'androidx.test:runner:1.1.0' + androidTestImplementation 'androidx.test.ext:junit:1.0.0' androidTestImplementation "org.mockito:mockito-android:2.23.0" + androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f661c2c6c..b5c6e5a2b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,14 @@ android:launchMode="singleTop" android:theme="@style/WulkanowyTheme.NoActionBar" /> + + + + + + diff --git a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt index 84885edcc..5aadf5f28 100644 --- a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt +++ b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt @@ -39,7 +39,7 @@ class WulkanowyApp : DaggerApplication() { private fun initializeFabric() { Fabric.with(Fabric.Builder(this) .kits(Crashlytics.Builder() - .core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()) + .core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG || !BuildConfig.FABRIC_ENABLED).build()) .build(), Answers()) .debuggable(BuildConfig.DEBUG) diff --git a/app/src/main/java/io/github/wulkanowy/data/ErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/data/ErrorHandler.kt index b4ba5a7f3..3ba1cd351 100644 --- a/app/src/main/java/io/github/wulkanowy/data/ErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/data/ErrorHandler.kt @@ -14,7 +14,7 @@ open class ErrorHandler @Inject constructor(private val resources: Resources) { var showErrorMessage: (String) -> Unit = {} open fun proceed(error: Throwable) { - Timber.i(error, "An exception occurred while the Wulkanowy was running") + Timber.e(error, "An exception occurred while the Wulkanowy was running") showErrorMessage((when (error) { is UnknownHostException -> resources.getString(R.string.all_no_internet) diff --git a/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefHelper.kt b/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefHelper.kt index 6adb79eb0..d4f9bb5ba 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefHelper.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefHelper.kt @@ -14,4 +14,12 @@ class SharedPrefHelper @Inject constructor(private val sharedPref: SharedPrefere fun getLong(key: String, defaultValue: Long): Long { return sharedPref.getLong(key, defaultValue) } -} \ No newline at end of file + + fun getBoolean(key: String, defaultValue: Boolean): Boolean { + return sharedPref.getBoolean(key, defaultValue) + } + + fun getString(key: String, defaultValue: String): String { + return sharedPref.getString(key, defaultValue) ?: defaultValue + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDao.kt index 6212b5b0c..e5a153a6c 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeDao.kt @@ -13,9 +13,15 @@ interface GradeDao { @Update fun update(grade: Grade) + @Update + fun updateAll(grade: List) + @Delete fun deleteAll(grades: List) @Query("SELECT * FROM Grades WHERE semester_id = :semesterId AND student_id = :studentId") fun getGrades(semesterId: Int, studentId: Int): Maybe> + + @Query("SELECT * FROM Grades WHERE is_read = 0 AND semester_id = :semesterId AND student_id = :studentId") + fun getNewGrades(semesterId: Int, studentId: Int): Maybe> } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeSummaryDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeSummaryDao.kt index dbbfa4df7..3a9b9d816 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeSummaryDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/GradeSummaryDao.kt @@ -11,7 +11,7 @@ import io.reactivex.Maybe @Dao interface GradeSummaryDao { - @Insert(onConflict = REPLACE) + @Insert fun insertAll(gradesSummary: List) @Delete diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/SemesterDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/SemesterDao.kt index e8e89406d..135e65e5b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/dao/SemesterDao.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/SemesterDao.kt @@ -14,4 +14,10 @@ interface SemesterDao { @Query("SELECT * FROM Semesters WHERE student_id = :studentId") fun getSemester(studentId: Int): Single> + + @Query("UPDATE Semesters SET is_current = 0") + fun resetCurrentSemester() + + @Query("UPDATE Semesters SET is_current = 1 WHERE semester_id = :semesterId") + fun setCurrentSemester(semesterId: Int) } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Grade.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Grade.kt index 44c161035..d665f9d20 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/Grade.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Grade.kt @@ -9,41 +9,44 @@ import java.io.Serializable @Entity(tableName = "Grades") data class Grade( - @ColumnInfo(name = "semester_id") - var semesterId: Int, + @ColumnInfo(name = "semester_id") + var semesterId: Int, - @ColumnInfo(name = "student_id") - var studentId: Int, + @ColumnInfo(name = "student_id") + var studentId: Int, - var subject: String, + var subject: String, - var entry: String, + var entry: String, - var value: Int, + var value: Int, - var modifier: Double, + var modifier: Double, - var comment: String, + var comment: String, - var color: String, + var color: String, - @ColumnInfo(name = "grade_symbol") - var gradeSymbol: String, + @ColumnInfo(name = "grade_symbol") + var gradeSymbol: String, - var description: String, + var description: String, - var weight: String, + var weight: String, - var weightValue: Int, + var weightValue: Int, - var date: LocalDate, + var date: LocalDate, - var teacher: String + var teacher: String ) : Serializable { @PrimaryKey(autoGenerate = true) var id: Long = 0 - @ColumnInfo(name = "is_new") - var isNew: Boolean = false + @ColumnInfo(name = "is_read") + var isRead: Boolean = true + + @ColumnInfo(name = "is_notified") + var isNotified: Boolean = true } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt index 9e4e2ab89..42266955e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/GradeRepository.kt @@ -14,29 +14,39 @@ import javax.inject.Singleton @Singleton class GradeRepository @Inject constructor( - private val settings: InternetObservingSettings, - private val local: GradeLocal, - private val remote: GradeRemote + private val settings: InternetObservingSettings, + private val local: GradeLocal, + private val remote: GradeRemote ) { - fun getGrades(semester: Semester, forceRefresh: Boolean = false): Single> { + fun getGrades(semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Single> { return local.getGrades(semester).filter { !forceRefresh } - .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) - .flatMap { - if (it) remote.getGrades(semester) - else Single.error(UnknownHostException()) - }.flatMap { newGrades -> - local.getGrades(semester).toSingle(emptyList()) - .doOnSuccess { oldGrades -> - local.deleteGrades(oldGrades - newGrades) - local.saveGrades((newGrades - oldGrades) - .onEach { if (oldGrades.isNotEmpty()) it.isNew = true }) - } - }.flatMap { local.getGrades(semester).toSingle(emptyList()) }) + .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) + .flatMap { + if (it) remote.getGrades(semester) + else Single.error(UnknownHostException()) + }.flatMap { newGrades -> + local.getGrades(semester).toSingle(emptyList()) + .doOnSuccess { oldGrades -> + local.deleteGrades(oldGrades - newGrades) + local.saveGrades((newGrades - oldGrades) + .onEach { + if (oldGrades.isNotEmpty()) it.isRead = false + if (notify) it.isNotified = false + }) + } + }.flatMap { local.getGrades(semester).toSingle(emptyList()) }) + } + fun getNewGrades(semester: Semester): Single> { + return local.getNewGrades(semester).toSingle(emptyList()) } fun updateGrade(grade: Grade): Completable { return local.updateGrade(grade) } + + fun updateGrades(grades: List): Completable { + return local.updateGrades(grades) + } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 0da419405..2c4a1242e 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -1,19 +1,39 @@ package io.github.wulkanowy.data.repositories +import android.content.Context import android.content.SharedPreferences +import io.github.wulkanowy.R import javax.inject.Inject import javax.inject.Singleton @Singleton -class PreferencesRepository @Inject constructor(private val sharedPref: SharedPreferences) { +class PreferencesRepository @Inject constructor( + private val sharedPref: SharedPreferences, + val context: Context +) { val startMenuIndex: Int - get() = sharedPref.getString("start_menu", "0")?.toInt() ?: 0 + get() = sharedPref.getString(context.getString(R.string.pref_key_start_menu), "0")?.toInt() ?: 0 val showPresent: Boolean - get() = sharedPref.getBoolean("attendance_present", true) + get() = sharedPref.getBoolean(context.getString(R.string.pref_key_attendance_present), true) + val currentThemeKey: String = context.getString(R.string.pref_key_theme) val currentTheme: Int - get() = sharedPref.getString("theme", "1")?.toInt() ?: 1 -} + get() = sharedPref.getString(currentThemeKey, "1")?.toInt() ?: 1 + val serviceEnablesKey: String = context.getString(R.string.pref_key_services_enable) + val serviceEnabled: Boolean + get() = sharedPref.getBoolean(serviceEnablesKey, true) + + val servicesIntervalKey: String = context.getString(R.string.pref_key_services_interval) + val servicesInterval: Int + get() = sharedPref.getString(servicesIntervalKey, "60")?.toInt() ?: 60 + + val servicesOnlyWifiKey: String = context.getString(R.string.pref_key_services_wifi_only) + val servicesOnlyWifi: Boolean + get() = sharedPref.getBoolean(servicesOnlyWifiKey, true) + + val notificationsEnable: Boolean + get() = sharedPref.getBoolean(context.getString(R.string.pref_key_notifications_enable), true) +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/SessionRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/SessionRepository.kt index 7b0ea4c49..93c8e2b70 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/SessionRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/SessionRepository.kt @@ -14,9 +14,10 @@ import javax.inject.Singleton @Singleton class SessionRepository @Inject constructor( - private val local: SessionLocal, - private val remote: SessionRemote, - private val settings: InternetObservingSettings) { + private val local: SessionLocal, + private val remote: SessionRemote, + private val settings: InternetObservingSettings +) { val isSessionSaved get() = local.isSessionSaved @@ -26,24 +27,39 @@ class SessionRepository @Inject constructor( fun getConnectedStudents(email: String, password: String, symbol: String, endpoint: String): Single> { cachedStudents = ReactiveNetwork.checkInternetConnectivity(settings) - .flatMap { isConnected -> - if (isConnected) remote.getConnectedStudents(email, password, symbol, endpoint) - else Single.error>(UnknownHostException("No internet connection")) - }.doOnSuccess { cachedStudents = Single.just(it) } + .flatMap { isConnected -> + if (isConnected) remote.getConnectedStudents(email, password, symbol, endpoint) + else Single.error>(UnknownHostException("No internet connection")) + }.doOnSuccess { cachedStudents = Single.just(it) } return cachedStudents } - fun getSemesters(): Single> { - return local.getLastStudent() - .flatMapSingle { - remote.initApi(it, true) - local.getSemesters(it) - } + fun saveStudent(student: Student): Completable { + return remote.getSemesters(student) + .flatMapCompletable { local.saveSemesters(it) } + .concatWith(local.saveStudent(student)) } - fun saveStudent(student: Student): Completable { - return remote.getSemesters(student).flatMapCompletable { local.saveSemesters(it) } - .concatWith(local.saveStudent(student)) + fun getSemesters(forceRefresh: Boolean = false): Single> { + return local.getLastStudent() + .flatMapSingle { student -> + remote.initApi(student) + local.getSemesters(student).filter { !forceRefresh } + .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap { + if (it) remote.getCurrentSemester(student) + else Single.error(UnknownHostException()) + }.flatMap { current -> + local.getSemesters(student).doOnSuccess { semesters -> + if (semesters.single { it.current }.semesterId != current.semesterId) { + local.saveSemesters(listOf(current)).andThen { + local.setCurrentSemester(current.semesterId) + } + } + } + }.flatMap { + local.getSemesters(student) + }) + } } fun clearCache() { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/local/GradeLocal.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/local/GradeLocal.kt index 5689ee4bb..4110bc3a2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/local/GradeLocal.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/local/GradeLocal.kt @@ -15,6 +15,10 @@ class GradeLocal @Inject constructor(private val gradeDb: GradeDao) { return gradeDb.getGrades(semester.semesterId, semester.studentId).filter { !it.isEmpty() } } + fun getNewGrades(semester: Semester): Maybe> { + return gradeDb.getNewGrades(semester.semesterId, semester.studentId) + } + fun saveGrades(grades: List) { gradeDb.insertAll(grades) } @@ -23,6 +27,10 @@ class GradeLocal @Inject constructor(private val gradeDb: GradeDao) { return Completable.fromCallable { gradeDb.update(grade) } } + fun updateGrades(grade: List): Completable { + return Completable.fromCallable { gradeDb.updateAll(grade) } + } + fun deleteGrades(grades: List) { gradeDb.deleteAll(grades) } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/local/SessionLocal.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/local/SessionLocal.kt index b26ce6371..008cd8e3f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/local/SessionLocal.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/local/SessionLocal.kt @@ -46,4 +46,10 @@ class SessionLocal @Inject constructor( fun getSemesters(student: Student): Single> { return semesterDb.getSemester(student.studentId) } + + fun setCurrentSemester(semesterId: Int): Completable { + return Single.fromCallable { semesterDb.resetCurrentSemester() }.ignoreElement().andThen { + semesterDb.setCurrentSemester(semesterId) + } + } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/remote/SessionRemote.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/remote/SessionRemote.kt index a02b0ada6..f01c677ba 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/remote/SessionRemote.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/remote/SessionRemote.kt @@ -12,37 +12,46 @@ import javax.inject.Singleton class SessionRemote @Inject constructor(private val api: Api) { fun getConnectedStudents(email: String, password: String, symbol: String, endpoint: String): Single> { - return Single.just(initApi(Student(email = email, password = password, symbol = symbol, endpoint = endpoint, loginType = "AUTO"))) - .flatMap { _ -> - api.getPupils().map { students -> - students.map { - Student( - email = email, - password = password, - symbol = it.symbol, - studentId = it.studentId, - studentName = it.studentName, - schoolSymbol = it.schoolSymbol, - schoolName = it.schoolName, - endpoint = endpoint, - loginType = it.loginType.name - ) - } - } + return Single.just( + initApi( + Student( + email = email, + password = password, + symbol = symbol, + endpoint = endpoint, + loginType = "AUTO" + ), true + ) + ).flatMap { + api.getPupils().map { students -> + students.map { pupil -> + Student( + email = email, + password = password, + symbol = pupil.symbol, + studentId = pupil.studentId, + studentName = pupil.studentName, + schoolSymbol = pupil.schoolSymbol, + schoolName = pupil.schoolName, + endpoint = endpoint, + loginType = pupil.loginType.name + ) } + } + } } fun getSemesters(student: Student): Single> { - return Single.just(initApi(student)).flatMap { _ -> + return Single.just(initApi(student)).flatMap { api.getSemesters().map { semesters -> - semesters.map { + semesters.map { semester -> Semester( - studentId = student.studentId, - diaryId = it.diaryId, - diaryName = it.diaryName, - semesterId = it.semesterId, - semesterName = it.semesterNumber, - current = it.current + studentId = student.studentId, + diaryId = semester.diaryId, + diaryName = semester.diaryName, + semesterId = semester.semesterId, + semesterName = semester.semesterNumber, + current = semester.current ) } @@ -50,8 +59,21 @@ class SessionRemote @Inject constructor(private val api: Api) { } } - fun initApi(student: Student, checkInit: Boolean = false) { - if (if (checkInit) 0 == api.studentId else true) { + fun getCurrentSemester(student: Student): Single { + return api.getCurrentSemester().map { + Semester( + studentId = student.studentId, + diaryId = it.diaryId, + diaryName = it.diaryName, + semesterId = it.semesterId, + semesterName = it.semesterNumber, + current = it.current + ) + } + } + + fun initApi(student: Student, reInitialize: Boolean = false) { + if (if (reInitialize) true else 0 == api.studentId) { api.run { email = student.email password = student.password diff --git a/app/src/main/java/io/github/wulkanowy/di/AppComponent.kt b/app/src/main/java/io/github/wulkanowy/di/AppComponent.kt index 053e46976..21c193e5a 100644 --- a/app/src/main/java/io/github/wulkanowy/di/AppComponent.kt +++ b/app/src/main/java/io/github/wulkanowy/di/AppComponent.kt @@ -14,6 +14,7 @@ import javax.inject.Singleton RepositoryModule::class, BuilderModule::class]) interface AppComponent : AndroidInjector { + @Component.Builder abstract class Builder : AndroidInjector.Builder() } diff --git a/app/src/main/java/io/github/wulkanowy/di/AppModule.kt b/app/src/main/java/io/github/wulkanowy/di/AppModule.kt index e6a8b6492..430b5c293 100644 --- a/app/src/main/java/io/github/wulkanowy/di/AppModule.kt +++ b/app/src/main/java/io/github/wulkanowy/di/AppModule.kt @@ -1,6 +1,8 @@ package io.github.wulkanowy.di import android.content.Context +import com.firebase.jobdispatcher.FirebaseJobDispatcher +import com.firebase.jobdispatcher.GooglePlayDriver import dagger.Module import dagger.Provides import eu.davidea.flexibleadapter.FlexibleAdapter @@ -22,4 +24,10 @@ internal class AppModule { @Provides fun provideFlexibleAdapter() = FlexibleAdapter>(null, null, true) + + @Singleton + @Provides + fun provideJobDispatcher(context: Context): FirebaseJobDispatcher { + return FirebaseJobDispatcher(GooglePlayDriver(context)) + } } diff --git a/app/src/main/java/io/github/wulkanowy/di/BuilderModule.kt b/app/src/main/java/io/github/wulkanowy/di/BuilderModule.kt index 66adcd554..694580f49 100644 --- a/app/src/main/java/io/github/wulkanowy/di/BuilderModule.kt +++ b/app/src/main/java/io/github/wulkanowy/di/BuilderModule.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.di import dagger.Module import dagger.android.ContributesAndroidInjector import io.github.wulkanowy.di.scopes.PerActivity +import io.github.wulkanowy.services.job.SyncWorker import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginModule import io.github.wulkanowy.ui.modules.main.MainActivity @@ -23,4 +24,7 @@ internal abstract class BuilderModule { @PerActivity @ContributesAndroidInjector(modules = [MainModule::class]) abstract fun bindMainActivity(): MainActivity + + @ContributesAndroidInjector + abstract fun bindSyncJob(): SyncWorker } diff --git a/app/src/main/java/io/github/wulkanowy/services/job/ServiceHelper.kt b/app/src/main/java/io/github/wulkanowy/services/job/ServiceHelper.kt new file mode 100644 index 000000000..ab88edd86 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/job/ServiceHelper.kt @@ -0,0 +1,57 @@ +package io.github.wulkanowy.services.job + +import com.firebase.jobdispatcher.Constraint.ON_ANY_NETWORK +import com.firebase.jobdispatcher.Constraint.ON_UNMETERED_NETWORK +import com.firebase.jobdispatcher.FirebaseJobDispatcher +import com.firebase.jobdispatcher.Lifetime.FOREVER +import com.firebase.jobdispatcher.RetryStrategy.DEFAULT_EXPONENTIAL +import com.firebase.jobdispatcher.Trigger.executionWindow +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.utils.isHolidays +import org.threeten.bp.LocalDate +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ServiceHelper @Inject constructor( + private val prefRepository: PreferencesRepository, + private val dispatcher: FirebaseJobDispatcher +) { + + fun reloadFullSyncService() { + startFullSyncService(true) + } + + fun startFullSyncService(replaceCurrent: Boolean = false) { + if (LocalDate.now().isHolidays || !prefRepository.serviceEnabled) { + Timber.d("Services disabled or it's holidays") + return + } + + dispatcher.mustSchedule( + dispatcher.newJobBuilder() + .setLifetime(FOREVER) + .setRecurring(true) + .setService(SyncWorker::class.java) + .setTag(SyncWorker.WORK_TAG) + .setTrigger( + executionWindow( + prefRepository.servicesInterval * 60, + (prefRepository.servicesInterval + 10) * 60 + ) + ) + .setConstraints(if (prefRepository.servicesOnlyWifi) ON_UNMETERED_NETWORK else ON_ANY_NETWORK) + .setReplaceCurrent(replaceCurrent) + .setRetryStrategy(DEFAULT_EXPONENTIAL) + .build() + ) + + Timber.d("Services started") + } + + fun stopFullSyncService() { + dispatcher.cancel(SyncWorker.WORK_TAG) + Timber.d("Services stopped") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt b/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt new file mode 100644 index 000000000..e9f446ac1 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt @@ -0,0 +1,110 @@ +package io.github.wulkanowy.services.job + +import com.firebase.jobdispatcher.JobParameters +import com.firebase.jobdispatcher.SimpleJobService +import dagger.android.AndroidInjection +import io.github.wulkanowy.data.repositories.AttendanceRepository +import io.github.wulkanowy.data.repositories.ExamRepository +import io.github.wulkanowy.data.repositories.GradeRepository +import io.github.wulkanowy.data.repositories.GradeSummaryRepository +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.repositories.SessionRepository +import io.github.wulkanowy.data.repositories.TimetableRepository +import io.github.wulkanowy.services.notification.GradeNotification +import io.github.wulkanowy.utils.friday +import io.github.wulkanowy.utils.isHolidays +import io.github.wulkanowy.utils.monday +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import org.threeten.bp.LocalDate +import timber.log.Timber +import javax.inject.Inject + +class SyncWorker : SimpleJobService() { + + @Inject + lateinit var session: SessionRepository + + @Inject + lateinit var gradesDetails: GradeRepository + + @Inject + lateinit var gradesSummary: GradeSummaryRepository + + @Inject + lateinit var attendance: AttendanceRepository + + @Inject + lateinit var exam: ExamRepository + + @Inject + lateinit var timetable: TimetableRepository + + @Inject + lateinit var prefRepository: PreferencesRepository + + private val disposable = CompositeDisposable() + + companion object { + const val WORK_TAG = "FULL_SYNC" + } + + override fun onCreate() { + super.onCreate() + AndroidInjection.inject(this) + } + + override fun onRunJob(job: JobParameters?): Int { + Timber.d("Synchronization started") + + val start = LocalDate.now().monday + val end = LocalDate.now().friday + + if (start.isHolidays) return RESULT_FAIL_NORETRY + + var error: Throwable? = null + + disposable.add(session.getSemesters(true) + .map { it.single { semester -> semester.current } } + .flatMapPublisher { + Single.merge( + listOf( + gradesDetails.getGrades(it, true, true), + gradesSummary.getGradesSummary(it, true), + attendance.getAttendance(it, start, end, true), + exam.getExams(it, start, end, true), + timetable.getTimetable(it, start, end, true) + ) + ) + } + .subscribe({}, { error = it })) + + return if (null === error) { + if (prefRepository.notificationsEnable) sendNotifications() + Timber.d("Synchronization successful") + RESULT_SUCCESS + } else { + Timber.e(error, "Synchronization failed") + RESULT_FAIL_RETRY + } + } + + private fun sendNotifications() { + disposable.add(session.getSemesters(true) + .map { it.single { semester -> semester.current } } + .flatMap { gradesDetails.getNewGrades(it) } + .map { it.filter { grade -> !grade.isNotified } } + .subscribe({ + if (it.isNotEmpty()) { + Timber.d("Found ${it.size} unread grades") + GradeNotification(applicationContext).sendNotification(it) + gradesDetails.updateGrades(it.map { grade -> grade.apply { isNotified = true } }).subscribe() + } + }) { Timber.e("Notifications sending failed") }) + } + + override fun onDestroy() { + super.onDestroy() + disposable.clear() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/notification/BaseNotification.kt b/app/src/main/java/io/github/wulkanowy/services/notification/BaseNotification.kt new file mode 100644 index 000000000..945d0b153 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/notification/BaseNotification.kt @@ -0,0 +1,34 @@ +package io.github.wulkanowy.services.notification + +import android.app.Notification +import android.app.NotificationManager +import android.content.Context +import android.content.Context.NOTIFICATION_SERVICE +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES.O +import androidx.core.app.NotificationCompat +import timber.log.Timber +import kotlin.random.Random + +abstract class BaseNotification(protected val context: Context) { + + protected val notificationManager: NotificationManager by lazy { + context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + } + + fun notify(notification: Notification) { + notificationManager.notify(Random.nextInt(1000), notification) + } + + fun notificationBuilder(channelId: String): NotificationCompat.Builder { + if (SDK_INT >= O) createChannel(channelId) + return NotificationCompat.Builder(context, channelId) + } + + fun cancelAll() { + notificationManager.cancelAll() + Timber.d("Notifications canceled") + } + + abstract fun createChannel(channelId: String) +} diff --git a/app/src/main/java/io/github/wulkanowy/services/notification/GradeNotification.kt b/app/src/main/java/io/github/wulkanowy/services/notification/GradeNotification.kt new file mode 100644 index 000000000..8eec6a20e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/notification/GradeNotification.kt @@ -0,0 +1,60 @@ +package io.github.wulkanowy.services.notification + +import android.annotation.TargetApi +import android.app.Notification.VISIBILITY_PUBLIC +import android.app.NotificationChannel +import android.app.NotificationManager.IMPORTANCE_HIGH +import android.app.PendingIntent +import android.app.PendingIntent.FLAG_UPDATE_CURRENT +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.Grade +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.main.MainActivity.Companion.EXTRA_CARD_ID_KEY +import timber.log.Timber + +class GradeNotification(context: Context) : BaseNotification(context) { + + private val channelId = "Grade_Notify" + + @TargetApi(26) + override fun createChannel(channelId: String) { + notificationManager.createNotificationChannel(NotificationChannel( + channelId, context.getString(R.string.notify_grade_channel), IMPORTANCE_HIGH + ).apply { + enableLights(true) + enableVibration(true) + lockscreenVisibility = VISIBILITY_PUBLIC + }) + } + + fun sendNotification(items: List) { + notify(notificationBuilder(channelId) + .setContentTitle(context.resources.getQuantityString(R.plurals.grade_new_items, items.size, items.size)) + .setContentText(context.resources.getQuantityString(R.plurals.notify_grade_new_items, items.size, items.size)) + .setSmallIcon(R.drawable.ic_stat_notify_grade) + .setAutoCancel(true) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setColor(ContextCompat.getColor(context, R.color.colorPrimary)) + .setContentIntent( + PendingIntent.getActivity(context, 0, + MainActivity.getStartIntent(context).putExtra(EXTRA_CARD_ID_KEY, 0), + FLAG_UPDATE_CURRENT + ) + ) + .setStyle(NotificationCompat.InboxStyle().run { + setSummaryText(context.resources.getQuantityString(R.plurals.grade_number_item, items.size, items.size)) + items.forEach { + addLine("${it.subject}: ${it.entry}") + } + this + }) + .build() + ) + + Timber.d("Notification sent") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt index 55816e409..9a0b5eda0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/base/BasePresenter.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.base import io.github.wulkanowy.data.ErrorHandler +import io.github.wulkanowy.ui.modules.main.MainView import io.reactivex.disposables.CompositeDisposable open class BasePresenter(private val errorHandler: ErrorHandler) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutModule.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutModule.kt index 675026659..cc5ba7cf3 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutModule.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutModule.kt @@ -12,4 +12,3 @@ class AboutModule { @Provides fun provideLibsFragmentCompat() = LibsFragmentCompat() } - diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutView.kt index 1e05bfc54..a3ae2ada8 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/about/AboutView.kt @@ -7,4 +7,4 @@ interface AboutView : BaseView { fun openSourceWebView() fun openIssuesWebView() -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeModule.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeModule.kt index 186ab4db6..eb032f795 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeModule.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeModule.kt @@ -29,4 +29,3 @@ abstract class GradeModule { @ContributesAndroidInjector abstract fun binGradeSummaryFragment(): GradeSummaryFragment } - diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt index 323db812b..19b462b1b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsDialog.kt @@ -75,6 +75,4 @@ class GradeDetailsDialog : DialogFragment() { gradeDialogClose.setOnClickListener { dismiss() } } - } - diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsItem.kt index bb3480c02..18c2656d6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsItem.kt @@ -34,7 +34,7 @@ class GradeDetailsItem(val grade: Grade, private val weightString: String, priva gradeItemDescription.text = if (grade.description.isNotBlank()) grade.description else grade.gradeSymbol gradeItemDate.text = grade.date.toFormattedString() gradeItemWeight.text = "$weightString: ${grade.weight}" - gradeItemNote.visibility = if (grade.isNew) VISIBLE else GONE + gradeItemNote.visibility = if (!grade.isRead) VISIBLE else GONE } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt index 20e1a7f83..5e58b2f4f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/details/GradeDetailsPresenter.kt @@ -9,6 +9,7 @@ import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.calcAverage import io.github.wulkanowy.utils.valueColor +import timber.log.Timber import javax.inject.Inject class GradeDetailsPresenter @Inject constructor( @@ -51,8 +52,8 @@ class GradeDetailsPresenter @Inject constructor( if (item is GradeDetailsItem) { view?.apply { showGradeDialog(item.grade) - if (item.grade.isNew) { - item.grade.isNew = false + if (!item.grade.isRead) { + item.grade.isRead = true updateItem(item) getHeaderOfItem(item)?.let { header -> if (header is GradeDetailsHeader) { @@ -94,7 +95,7 @@ class GradeDetailsPresenter @Inject constructor( subject = it.key, average = formatAverage(average), number = view?.getGradeNumberString(it.value.size).orEmpty(), - newGrades = it.value.filter { grade -> grade.isNew }.size + newGrades = it.value.filter { grade -> !grade.isRead }.size ).apply { subItems = it.value.map { item -> GradeDetailsItem( @@ -120,5 +121,6 @@ class GradeDetailsPresenter @Inject constructor( .subscribeOn(schedulers.backgroundThread) .observeOn(schedulers.mainThread) .subscribe({}) { error -> errorHandler.proceed(error) }) + Timber.d("Grade ${grade.id} updated") } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt index ab12479c2..418dbed93 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginErrorHandler.kt @@ -20,4 +20,3 @@ class LoginErrorHandler(resources: Resources) : ErrorHandler(resources) { doOnBadCredentials = {} } } - diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginSwitchListener.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginSwitchListener.kt index 564d1df95..1e6d82c8d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginSwitchListener.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/LoginSwitchListener.kt @@ -3,4 +3,4 @@ package io.github.wulkanowy.ui.modules.login interface LoginSwitchListener { fun switchFragment(position: Int) -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt index f6b0cf3cc..13c11d288 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainActivity.kt @@ -10,6 +10,7 @@ import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem import com.ncapdevi.fragnav.FragNavController import com.ncapdevi.fragnav.FragNavController.Companion.HIDE import io.github.wulkanowy.R +import io.github.wulkanowy.services.notification.GradeNotification import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment @@ -31,6 +32,7 @@ class MainActivity : BaseActivity(), MainView { lateinit var navController: FragNavController companion object { + const val EXTRA_CARD_ID_KEY = "cardId" fun getStartIntent(context: Context) = Intent(context, MainActivity::class.java) } @@ -51,7 +53,7 @@ class MainActivity : BaseActivity(), MainView { setSupportActionBar(mainToolbar) messageContainer = mainFragmentContainer - presenter.onAttachView(this) + presenter.onAttachView(this, intent.getIntExtra(EXTRA_CARD_ID_KEY, -1)) navController.initialize(startMenuIndex, savedInstanceState) } @@ -66,13 +68,15 @@ class MainActivity : BaseActivity(), MainView { override fun initView() { mainBottomNav.run { - addItems(mutableListOf( + addItems( + mutableListOf( AHBottomNavigationItem(R.string.grade_title, R.drawable.ic_menu_main_grade_26dp, 0), AHBottomNavigationItem(R.string.attendance_title, R.drawable.ic_menu_main_attendance_24dp, 0), AHBottomNavigationItem(R.string.exam_title, R.drawable.ic_menu_main_exam_24dp, 0), AHBottomNavigationItem(R.string.timetable_title, R.drawable.ic_menu_main_timetable_24dp, 0), AHBottomNavigationItem(R.string.more_title, R.drawable.ic_menu_main_more_24dp, 0) - )) + ) + ) accentColor = ContextCompat.getColor(context, R.color.colorPrimary) inactiveColor = getThemeAttrColor(android.R.attr.textColorSecondary) defaultBackgroundColor = getThemeAttrColor(R.attr.bottomNavBackground) @@ -89,11 +93,11 @@ class MainActivity : BaseActivity(), MainView { setOnViewChangeListener { presenter.onViewStart() } fragmentHideStrategy = HIDE rootFragments = listOf( - GradeFragment.newInstance(), - AttendanceFragment.newInstance(), - ExamFragment.newInstance(), - TimetableFragment.newInstance(), - MoreFragment.newInstance() + GradeFragment.newInstance(), + AttendanceFragment.newInstance(), + ExamFragment.newInstance(), + TimetableFragment.newInstance(), + MoreFragment.newInstance() ) } } @@ -126,6 +130,10 @@ class MainActivity : BaseActivity(), MainView { presenter.onBackPressed { super.onBackPressed() } } + override fun cancelNotifications() { + GradeNotification(applicationContext).cancelAll() + } + override fun onSaveInstanceState(outState: Bundle?) { super.onSaveInstanceState(outState) navController.onSaveInstanceState(outState) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt index 959f482da..278c3f0b1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt @@ -14,6 +14,7 @@ import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeModule import io.github.wulkanowy.ui.modules.more.MoreFragment +import io.github.wulkanowy.ui.modules.settings.SettingsFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment @Module @@ -53,4 +54,7 @@ abstract class MainModule { @PerFragment @ContributesAndroidInjector(modules = [AboutModule::class]) abstract fun bindAboutFragment(): AboutFragment + + @ContributesAndroidInjector + abstract fun bindSettingsFragment(): SettingsFragment } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt index 4cb716272..aa696f189 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainPresenter.kt @@ -2,20 +2,26 @@ package io.github.wulkanowy.ui.modules.main import io.github.wulkanowy.data.ErrorHandler import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.services.job.ServiceHelper import io.github.wulkanowy.ui.base.BasePresenter import javax.inject.Inject class MainPresenter @Inject constructor( - errorHandler: ErrorHandler, - private val prefRepository: PreferencesRepository) - : BasePresenter(errorHandler) { + errorHandler: ErrorHandler, + private val prefRepository: PreferencesRepository, + private val serviceHelper: ServiceHelper +) : BasePresenter(errorHandler) { - override fun onAttachView(view: MainView) { + fun onAttachView(view: MainView, init: Int) { super.onAttachView(view) + view.run { - startMenuIndex = prefRepository.startMenuIndex + cancelNotifications() + startMenuIndex = if (init != -1) init else prefRepository.startMenuIndex initView() } + + serviceHelper.startFullSyncService() } fun onViewStart() { @@ -33,7 +39,6 @@ class MainPresenter @Inject constructor( return true } - fun onBackPressed(default: () -> Unit) { view?.run { if (isRootView) default() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt index 9a5121d53..8848f2f06 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainView.kt @@ -24,6 +24,8 @@ interface MainView : BaseView { fun popView() + fun cancelNotifications() + interface MainChildView { fun onFragmentReselected() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt index 4061c4b33..7a24a10d0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsFragment.kt @@ -1,14 +1,21 @@ package io.github.wulkanowy.ui.modules.settings +import android.content.Context import android.content.SharedPreferences import android.os.Bundle +import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate import com.takisoft.preferencex.PreferenceFragmentCompat +import dagger.android.support.AndroidSupportInjection import io.github.wulkanowy.R import io.github.wulkanowy.ui.modules.main.MainView +import javax.inject.Inject class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener, - MainView.TitledView { + MainView.TitledView, SettingsView { + + @Inject + lateinit var presenter: SettingsPresenter companion object { fun newInstance() = SettingsFragment() @@ -17,19 +24,40 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP override val titleStringId: Int get() = R.string.settings_title + override fun onAttach(context: Context) { + AndroidSupportInjection.inject(this) + super.onAttach(context) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + presenter.onAttachView(this) + } + override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.scheme_preferences) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String) { - when(key) { - "theme" -> { - AppCompatDelegate.setDefaultNightMode(sharedPreferences?.getString("theme", "1")?.toInt() ?: 1) - activity?.recreate() - } + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + presenter.onSharedPreferenceChanged(key) + } + + override fun setTheme(theme: Int) { + AppCompatDelegate.setDefaultNightMode(theme) + activity?.recreate() + } + + override fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean) { + findPreference(serviceEnablesKey).run { + summary = if (isHolidays) getString(R.string.pref_services_suspended) else "" + isEnabled = !isHolidays } } + override fun showMessage(text: String) { + Toast.makeText(context, text, Toast.LENGTH_SHORT).show() + } + override fun onResume() { super.onResume() preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt new file mode 100644 index 000000000..d3aa027c9 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsPresenter.kt @@ -0,0 +1,40 @@ +package io.github.wulkanowy.ui.modules.settings + +import io.github.wulkanowy.data.ErrorHandler +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.services.job.ServiceHelper +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.utils.isHolidays +import org.threeten.bp.LocalDate.now +import javax.inject.Inject + +class SettingsPresenter @Inject constructor( + errorHandler: ErrorHandler, + private val preferencesRepository: PreferencesRepository, + private val serviceHelper: ServiceHelper +) : BasePresenter(errorHandler) { + + override fun onAttachView(view: SettingsView) { + super.onAttachView(view) + + view.run { + setServicesSuspended(preferencesRepository.serviceEnablesKey, now().isHolidays) + } + } + + fun onSharedPreferenceChanged(key: String) { + when (key) { + preferencesRepository.serviceEnablesKey -> { + if (preferencesRepository.serviceEnabled) serviceHelper.startFullSyncService() + else serviceHelper.stopFullSyncService() + } + preferencesRepository.servicesIntervalKey, + preferencesRepository.servicesOnlyWifiKey -> { + serviceHelper.reloadFullSyncService() + } + preferencesRepository.currentThemeKey -> { + view?.setTheme(preferencesRepository.currentTheme) + } + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt new file mode 100644 index 000000000..0b3c2f70c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/SettingsView.kt @@ -0,0 +1,10 @@ +package io.github.wulkanowy.ui.modules.settings + +import io.github.wulkanowy.ui.base.BaseView + +interface SettingsView : BaseView { + + fun setTheme(theme: Int) + + fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt index be481b9fd..0b6e86bb0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/splash/SplashActivity.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.splash import android.os.Bundle +import io.github.wulkanowy.services.notification.GradeNotification import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.main.MainActivity diff --git a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt index 6e64c2b7c..6c7f242ce 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/security/Scrambler.kt @@ -140,4 +140,3 @@ private fun generateKeyPair(context: Context) { } Timber.i("A new KeyPair has been generated") } - diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index fb6d913d2..4a7ee27a9 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -62,6 +62,16 @@ + + Nowe oceny + + Dostałeś %1$d ocenę + "Dostałeś %1$d oceny + Dostałeś %1$d ocen + Dostałeś %1$d ocen + + + Lekcja Sala @@ -128,24 +138,12 @@ Powiadomienia Pokazuj powiadomienia - Usługi + Synchronizacja Automatyczna aktualizacja - Zawieszone na wakacjach + Zawieszona na wakacjach Interwał aktualizacji Tylko WiFi - Wymagany restart - - - - Nowe oceny - - Dostałeś %1$d ocenę - "Dostałeś %1$d oceny - Dostałeś %1$d ocen - Dostałeś %1$d ocen - - Czarny diff --git a/app/src/main/res/values-pl/value_prefernces.xml b/app/src/main/res/values-pl/value_prefernces.xml index 3731be408..611add4b2 100644 --- a/app/src/main/res/values-pl/value_prefernces.xml +++ b/app/src/main/res/values-pl/value_prefernces.xml @@ -1,9 +1,9 @@ - 10 minut + 15 minut 30 minut - 1 godzinę + 1 godzina 2 godziny 6 godzin 12 godzin diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml new file mode 100644 index 000000000..974d82424 --- /dev/null +++ b/app/src/main/res/values/preferences_keys.xml @@ -0,0 +1,10 @@ + + + start_menu + attendance_present + theme + services_enable + services_interval + services_disable_wifi_only + notifications_enable + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4dc703fba..61e45d6ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,6 +57,13 @@ New grades + + New grades + + You received %1$d grade + You received %1$d grades + + Lesson @@ -124,22 +131,12 @@ Notifications Show notifications - Services + Synchronization Automatic update Suspended on holiday Updates interval Only WiFi - Restart required - - - - New grades - - You received %1$d grade - You received %1$d grades - - Black diff --git a/app/src/main/res/values/value_prefernces.xml b/app/src/main/res/values/value_prefernces.xml index b855e24c5..bf07b5158 100644 --- a/app/src/main/res/values/value_prefernces.xml +++ b/app/src/main/res/values/value_prefernces.xml @@ -27,7 +27,7 @@ - 10 minutes + 15 minutes 30 minutes 1 hour 2 hours @@ -36,7 +36,7 @@ 24 hours - 10 + 15 30 60 120 diff --git a/app/src/main/res/xml/scheme_preferences.xml b/app/src/main/res/xml/scheme_preferences.xml index 36180c94b..e39125555 100644 --- a/app/src/main/res/xml/scheme_preferences.xml +++ b/app/src/main/res/xml/scheme_preferences.xml @@ -1,29 +1,63 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> + android:title="@string/pref_view_header" + app:iconSpaceReserved="false"> + android:defaultValue="0" + android:entries="@array/startup_tab_entries" + android:entryValues="@array/startup_tab_value" + android:key="@string/pref_key_start_menu" + android:summary="%s" + android:title="@string/pref_view_list" + app:iconSpaceReserved="false" /> + + android:defaultValue="true" + android:key="@string/pref_key_attendance_present" + android:title="@string/pref_view_present" + app:iconSpaceReserved="false" /> + + + + android:defaultValue="60" + android:dependency="services_enable" + android:entries="@array/services_interval_entries" + android:entryValues="@array/services_interval_value" + android:key="@string/pref_key_services_interval" + android:summary="%s" + android:title="@string/pref_services_interval" + app:iconSpaceReserved="false" /> + + + + diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt index 7071fa27e..9d65f1882 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/main/MainPresenterTest.kt @@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.main import io.github.wulkanowy.data.ErrorHandler import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.services.job.ServiceHelper import org.junit.Before import org.junit.Test import org.mockito.Mock @@ -17,6 +18,9 @@ class MainPresenterTest { @Mock lateinit var prefRepository: PreferencesRepository + @Mock + lateinit var serviceHelper: ServiceHelper + @Mock lateinit var mainView: MainView @@ -27,8 +31,8 @@ class MainPresenterTest { MockitoAnnotations.initMocks(this) clearInvocations(mainView) - presenter = MainPresenter(errorHandler, prefRepository) - presenter.onAttachView(mainView) + presenter = MainPresenter(errorHandler, prefRepository, serviceHelper) + presenter.onAttachView(mainView, -1) } @Test diff --git a/build.gradle b/build.gradle index d8960364a..1b4538bb6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.2.71' + ext.kotlin_version = '1.3.0' repositories { mavenCentral() google()