1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-20 00:49:10 -05:00

Merge branch 'release/0.16.0'

This commit is contained in:
Mikołaj Pich 2020-03-05 09:47:37 +01:00
commit 5de2e9afbd
156 changed files with 5807 additions and 965 deletions

1
.gitignore vendored
View File

@ -63,6 +63,7 @@ captures/
.idea/dynamic.xml
.idea/uiDesigner.xml
.idea/runConfigurations.xml
.idea/discord.xml
# Keystore files
*.jks

View File

@ -1,9 +1,6 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>

View File

@ -4,7 +4,7 @@ jdk: oraclejdk8
env:
global:
- ANDROID_API_LEVEL=29
- ANDROID_BUILD_TOOLS_VERSION=29.0.2
- ANDROID_BUILD_TOOLS_VERSION=29.0.3
cache:
directories:
@ -14,7 +14,7 @@ cache:
branches:
only:
- develop
- 0.15.0
- 0.16.0
android:
licenses:
@ -49,9 +49,9 @@ script:
- ./gradlew dependencies --stacktrace --daemon
- fossa --no-ansi || true
#- ./gradlew lintPlayRelease -x fabricGenerateResourcesPlayRelease --stacktrace --daemon
- ./gradlew testPlayDebugUnitTest -x fabricGenerateResourcesPlay --stacktrace --daemon
- ./gradlew createFdroidDebugCoverageReport --stacktrace --daemon
- ./gradlew jacocoTestReport --stacktrace --daemon
- ./gradlew -Pcoverage testPlayDebugUnitTest -x fabricGenerateResourcesPlay --stacktrace --daemon
- ./gradlew -Pcoverage createFdroidDebugCoverageReport --stacktrace --daemon
- ./gradlew -Pcoverage jacocoTestReport --stacktrace --daemon
- if [ -z ${SONAR_HOST+x} ]; then echo "sonar scan skipped"; else
git fetch --unshallow;
./gradlew sonarqube -x test -x lint -x fabricGenerateResourcesPlayRelease -x fabricGenerateResourcesFdroidRelease -Dsonar.host.url=$SONAR_HOST -Dsonar.organization=$SONAR_ORG -Dsonar.login=$SONAR_KEY -Dsonar.branch.name=${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} --stacktrace --daemon;

View File

@ -10,15 +10,15 @@ apply from: 'hooks.gradle'
android {
compileSdkVersion 29
buildToolsVersion '29.0.2'
buildToolsVersion '29.0.3'
defaultConfig {
applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 16
minSdkVersion 17
targetSdkVersion 29
versionCode 52
versionName "0.15.0"
versionCode 53
versionName "0.16.0"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@ -61,7 +61,7 @@ android {
buildConfigField "boolean", "CRASHLYTICS_ENABLED", project.hasProperty("enableCrashlytics") ? "true" : "false"
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
testCoverageEnabled = true
testCoverageEnabled = project.hasProperty('coverage')
ext.enableCrashlytics = project.hasProperty("enableCrashlytics")
}
}
@ -110,9 +110,9 @@ play {
}
ext {
work_manager = "2.3.0"
room = "2.2.3"
dagger = "2.25.4"
work_manager = "2.3.2"
room = "2.2.4"
dagger = "2.26"
chucker = "2.0.4"
mockk = "1.9.2"
}
@ -122,14 +122,14 @@ configurations.all {
}
dependencies {
implementation "io.github.wulkanowy:sdk:0.15.0"
implementation "io.github.wulkanowy:sdk:0.16.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.core:core-ktx:1.2.0-rc01"
implementation "androidx.core:core-ktx:1.2.0"
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.appcompat:appcompat-resources:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.0"
implementation "androidx.fragment:fragment-ktx:1.2.2"
implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.multidex:multidex:2.0.1"
@ -139,7 +139,7 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03"
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.1.0-rc02"
implementation "com.google.android.material:material:1.1.0"
implementation "com.github.wulkanowy:material-chips-input:2.0.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation "me.zhanghai.android.materialprogressbar:library:1.6.1"
@ -167,18 +167,19 @@ dependencies {
implementation "com.github.pwittchen:reactivenetwork-rx2:3.0.6"
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxjava:2.2.17"
implementation "io.reactivex.rxjava2:rxjava:2.2.18"
implementation "com.google.code.gson:gson:2.8.6"
implementation "com.jakewharton.threetenabp:threetenabp:1.2.2"
implementation "com.jakewharton.timber:timber:4.7.1"
implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation "fr.bipi.treessence:treessence:0.3.2"
implementation "com.mikepenz:aboutlibraries-core:7.1.0"
implementation 'com.wdullaer:materialdatetimepicker:4.2.3'
implementation("io.coil-kt:coil:0.9.2")
implementation("io.coil-kt:coil:0.9.5")
playImplementation "com.google.firebase:firebase-core:17.2.2"
playImplementation "com.google.firebase:firebase-core:17.2.3"
playImplementation "com.crashlytics.sdk.android:crashlytics:2.10.1"
releaseImplementation "fr.o80.chucker:library-no-op:$chucker"
@ -189,7 +190,7 @@ dependencies {
testImplementation "junit:junit:4.13"
testImplementation "io.mockk:mockk:$mockk"
testImplementation "org.threeten:threetenbp:1.4.1"
testImplementation "org.mockito:mockito-inline:3.2.4"
testImplementation "org.mockito:mockito-inline:3.3.1"
androidTestImplementation "androidx.test:core:1.2.0"
androidTestImplementation "androidx.test:runner:1.2.0"
@ -197,7 +198,7 @@ dependencies {
androidTestImplementation "io.mockk:mockk-android:$mockk"
androidTestImplementation "androidx.room:room-testing:$room"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
androidTestImplementation "org.mockito:mockito-android:3.2.4"
androidTestImplementation "org.mockito:mockito-android:3.3.1"
}
apply plugin: 'com.google.gms.google-services'

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,6 @@ import io.github.wulkanowy.data.db.entities.Semester
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.of
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@ -104,45 +103,45 @@ class Migration13Test : AbstractMigrationTest() {
val db = helper.runMigrationsAndValidate(dbName, 13, true, Migration13())
val semesters1 = getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 1 AND class_id = 5")
assertTrue { semesters1.single { it.isCurrent }.isCurrent }
assertTrue { semesters1.single { it.second }.second }
semesters1[0].run {
assertFalse(isCurrent)
assertEquals(1, semesterId)
assertEquals(1, diaryId)
assertFalse(second)
assertEquals(1, first.semesterId)
assertEquals(1, first.diaryId)
}
semesters1[2].run {
assertFalse(isCurrent)
assertEquals(3, semesterId)
assertEquals(2, diaryId)
assertFalse(second)
assertEquals(3, first.semesterId)
assertEquals(2, first.diaryId)
}
semesters1[3].run {
assertTrue(isCurrent)
assertEquals(4, semesterId)
assertEquals(2, diaryId)
assertTrue(second)
assertEquals(4, first.semesterId)
assertEquals(2, first.diaryId)
}
getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let {
assertTrue { it.single { it.isCurrent }.isCurrent }
assertEquals(1970, it[0].schoolYear)
assertEquals(of(1970, 1, 1), it[0].end)
assertEquals(of(1970, 1, 1), it[0].start)
assertFalse(it[0].isCurrent)
assertFalse(it[1].isCurrent)
assertFalse(it[2].isCurrent)
assertTrue(it[3].isCurrent)
assertTrue { it.single { it.second }.second }
assertEquals(1970, it[0].first.schoolYear)
assertEquals(of(1970, 1, 1), it[0].first.end)
assertEquals(of(1970, 1, 1), it[0].first.start)
assertFalse(it[0].second)
assertFalse(it[1].second)
assertFalse(it[2].second)
assertTrue(it[3].second)
}
getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let {
assertTrue { it.single { it.isCurrent }.isCurrent }
assertFalse(it[0].isCurrent)
assertFalse(it[1].isCurrent)
assertFalse(it[2].isCurrent)
assertTrue(it[3].isCurrent)
assertTrue { it.single { it.second }.second }
assertFalse(it[0].second)
assertFalse(it[1].second)
assertFalse(it[2].second)
assertTrue(it[3].second)
}
}
private fun getSemesters(db: SupportSQLiteDatabase, query: String): List<Semester> {
val semesters = mutableListOf<Semester>()
private fun getSemesters(db: SupportSQLiteDatabase, query: String): List<Pair<Semester, Boolean>> {
val semesters = mutableListOf<Pair<Semester, Boolean>>()
val cursor = db.query(query)
if (cursor.moveToFirst()) {
@ -153,13 +152,12 @@ class Migration13Test : AbstractMigrationTest() {
diaryName = cursor.getString(3),
semesterId = cursor.getInt(4),
semesterName = cursor.getInt(5),
isCurrent = cursor.getInt(6) == 1,
classId = cursor.getInt(7),
unitId = cursor.getInt(8),
schoolYear = cursor.getInt(9),
start = Converters().timestampToDate(cursor.getLong(10))!!,
end = Converters().timestampToDate(cursor.getLong(11))!!
))
) to (cursor.getInt(6) == 1))
} while (cursor.moveToNext())
}
return semesters.toList()

View File

@ -10,8 +10,8 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.of
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class)
@ -35,19 +35,19 @@ class AttendanceLocalTest {
@Test
fun saveAndReadTest() {
attendanceLocal.saveAttendance(listOf(
Attendance(1, 2, 3, LocalDate.of(2018, 9, 10), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.ACCEPTED.name),
Attendance(1, 2, 3, LocalDate.of(2018, 9, 14), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.WAITING.name),
Attendance(1, 2, 3, LocalDate.of(2018, 9, 17), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.ACCEPTED.name)
Attendance(1, 2, 3, of(2018, 9, 10), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.ACCEPTED.name),
Attendance(1, 2, 3, of(2018, 9, 14), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.WAITING.name),
Attendance(1, 2, 3, of(2018, 9, 17), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.ACCEPTED.name)
))
val attendance = attendanceLocal
.getAttendance(Semester(1, 2, "", 1, 3, 2019, true, now(), now(), 1, 1),
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 14)
)
.blockingGet()
.getAttendance(Semester(1, 2, "", 1, 3, 2019, now(), now(), 1, 1),
of(2018, 9, 10),
of(2018, 9, 14)
)
.blockingGet()
assertEquals(2, attendance.size)
assertEquals(attendance[0].date, LocalDate.of(2018, 9, 10))
assertEquals(attendance[1].date, LocalDate.of(2018, 9, 14))
assertEquals(attendance[0].date, of(2018, 9, 10))
assertEquals(attendance[1].date, of(2018, 9, 14))
}
}

View File

@ -11,6 +11,8 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.of
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class)
@ -35,20 +37,20 @@ class CompletedLessonsLocalTest {
@Test
fun saveAndReadTest() {
completedLessonsLocal.saveCompletedLessons(listOf(
getCompletedLesson(LocalDate.of(2018, 9, 10), 1),
getCompletedLesson(LocalDate.of(2018, 9, 14), 2),
getCompletedLesson(LocalDate.of(2018, 9, 17), 3)
getCompletedLesson(of(2018, 9, 10), 1),
getCompletedLesson(of(2018, 9, 14), 2),
getCompletedLesson(of(2018, 9, 17), 3)
))
val completed = completedLessonsLocal
.getCompletedLessons(Semester(1, 2, "", 1, 3, 2019, true, LocalDate.now(), LocalDate.now(), 1, 1),
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 14)
.getCompletedLessons(Semester(1, 2, "", 1, 3, 2019, now(), now(), 1, 1),
of(2018, 9, 10),
of(2018, 9, 14)
)
.blockingGet()
assertEquals(2, completed.size)
assertEquals(completed[0].date, LocalDate.of(2018, 9, 10))
assertEquals(completed[1].date, LocalDate.of(2018, 9, 14))
assertEquals(completed[0].date, of(2018, 9, 10))
assertEquals(completed[1].date, of(2018, 9, 14))
}
private fun getCompletedLesson(date: LocalDate, number: Int): CompletedLesson {

View File

@ -10,7 +10,8 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.of
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class)
@ -34,19 +35,19 @@ class ExamLocalTest {
@Test
fun saveAndReadTest() {
examLocal.saveExams(listOf(
Exam(1, 2, LocalDate.of(2018, 9, 10), LocalDate.now(), "", "", "", "", "", ""),
Exam(1, 2, LocalDate.of(2018, 9, 14), LocalDate.now(), "", "", "", "", "", ""),
Exam(1, 2, LocalDate.of(2018, 9, 17), LocalDate.now(), "", "", "", "", "", "")
Exam(1, 2, of(2018, 9, 10), now(), "", "", "", "", "", ""),
Exam(1, 2, of(2018, 9, 14), now(), "", "", "", "", "", ""),
Exam(1, 2, of(2018, 9, 17), now(), "", "", "", "", "", "")
))
val exams = examLocal
.getExams(Semester(1, 2, "", 1, 3, 2019, true, LocalDate.now(), LocalDate.now(), 1, 1),
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 14)
)
.blockingGet()
.getExams(Semester(1, 2, "", 1, 3, 2019, now(), now(), 1, 1),
of(2018, 9, 10),
of(2018, 9, 14)
)
.blockingGet()
assertEquals(2, exams.size)
assertEquals(exams[0].date, LocalDate.of(2018, 9, 10))
assertEquals(exams[1].date, LocalDate.of(2018, 9, 14))
assertEquals(exams[0].date, of(2018, 9, 10))
assertEquals(exams[1].date, of(2018, 9, 14))
}
}

View File

@ -40,7 +40,7 @@ class GradeLocalTest {
createGradeLocal(3, 5.0, LocalDate.of(2019, 2, 28), "", 2)
))
val semester = Semester(1, 2, "", 2019, 2, 1, true, now(), now(), 1, 1)
val semester = Semester(1, 2, "", 2019, 2, 1, now(), now(), 1, 1)
val grades = gradeLocal
.getGrades(semester)

View File

@ -6,7 +6,6 @@ import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.SdkHelper
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
@ -15,9 +14,6 @@ import io.github.wulkanowy.sdk.Sdk
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.SpyK
import io.mockk.just
import io.mockk.runs
import io.reactivex.Single
import org.junit.After
import org.junit.Before

View File

@ -11,7 +11,7 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class)
@ -40,10 +40,7 @@ class GradeStatisticsLocalTest {
getGradeStatistics("Fizyka", 1, 2)
))
val stats = gradeStatisticsLocal.getGradesStatistics(
Semester(2, 2, "", 2019, 1, 2, true, LocalDate.now(), LocalDate.now(), 1, 1), false,
"Matematyka"
).blockingGet()
val stats = gradeStatisticsLocal.getGradesStatistics(getSemester(), false, "Matematyka").blockingGet()
assertEquals(1, stats.size)
assertEquals(stats[0].subject, "Matematyka")
}
@ -56,12 +53,11 @@ class GradeStatisticsLocalTest {
getGradeStatistics("Fizyka", 1, 2)
))
val stats = gradeStatisticsLocal.getGradesStatistics(
Semester(2, 2, "", 2019, 1, 2, true, LocalDate.now(), LocalDate.now(), 1, 1), false,
"Wszystkie"
).blockingGet()
assertEquals(1, stats.size)
val stats = gradeStatisticsLocal.getGradesStatistics(getSemester(), false, "Wszystkie").blockingGet()
assertEquals(3, stats.size)
assertEquals(stats[0].subject, "Wszystkie")
assertEquals(stats[1].subject, "Matematyka")
assertEquals(stats[2].subject, "Chemia")
}
@Test
@ -72,11 +68,8 @@ class GradeStatisticsLocalTest {
getGradePointsStatistics("Fizyka", 1, 2)
))
val stats = gradeStatisticsLocal.getGradesPointsStatistics(
Semester(2, 2, "", 2019, 1, 2, true, LocalDate.now(), LocalDate.now(), 1, 1),
"Matematyka"
).blockingGet()
with(stats) {
val stats = gradeStatisticsLocal.getGradesPointsStatistics(getSemester(), "Matematyka").blockingGet()
with(stats[0]) {
assertEquals(subject, "Matematyka")
assertEquals(others, 5.0)
assertEquals(student, 5.0)
@ -87,10 +80,7 @@ class GradeStatisticsLocalTest {
fun saveAndRead_subjectEmpty() {
gradeStatisticsLocal.saveGradesPointsStatistics(listOf())
val stats = gradeStatisticsLocal.getGradesPointsStatistics(
Semester(2, 2, "", 2019, 1, 2, true, LocalDate.now(), LocalDate.now(), 1, 1),
"Matematyka"
).blockingGet()
val stats = gradeStatisticsLocal.getGradesPointsStatistics(getSemester(), "Matematyka").blockingGet()
assertEquals(null, stats)
}
@ -98,13 +88,14 @@ class GradeStatisticsLocalTest {
fun saveAndRead_allEmpty() {
gradeStatisticsLocal.saveGradesPointsStatistics(listOf())
val stats = gradeStatisticsLocal.getGradesPointsStatistics(
Semester(2, 2, "", 2019, 1, 2, true, LocalDate.now(), LocalDate.now(), 1, 1),
"Wszystkie"
).blockingGet()
val stats = gradeStatisticsLocal.getGradesPointsStatistics(getSemester(), "Wszystkie").blockingGet()
assertEquals(null, stats)
}
private fun getSemester(): Semester {
return Semester(2, 2, "", 2019, 1, 2, now(), now(), 1, 1)
}
private fun getGradeStatistics(subject: String, studentId: Int, semesterId: Int): GradeStatistics {
return GradeStatistics(studentId, semesterId, subject, 5, 5, false)
}

View File

@ -6,11 +6,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime.now
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class)
@ -36,7 +38,7 @@ class LuckyNumberLocalTest {
fun saveAndReadTest() {
luckyNumberLocal.saveLuckyNumber(LuckyNumber(1, LocalDate.of(2019, 1, 20), 14))
val luckyNumber = luckyNumberLocal.getLuckyNumber(Semester(1, 1, "", 1, 3, 2019, true, LocalDate.now(), LocalDate.now(), 1, 1),
val luckyNumber = luckyNumberLocal.getLuckyNumber(Student("", "", "", "", "", "", false, "", "", "", 1, 1, "", "", "", "", "", 1, false, now()),
LocalDate.of(2019, 1, 20)
).blockingGet()

View File

@ -42,7 +42,7 @@ class RecipientLocalTest {
))
val recipients = recipientLocal.getRecipients(
Student("fakelog.cf", "AUTO", "", "", "", "", false, "", "", "", 1, 0, "", "", "", "", 1, true, LocalDateTime.now()),
Student("fakelog.cf", "AUTO", "", "", "", "", false, "", "", "", 1, 0, "", "", "", "", "", 1, true, LocalDateTime.now()),
2,
ReportingUnit(1, 4, "", 0, "", emptyList())
).blockingGet()

View File

@ -39,7 +39,7 @@ class StudentLocalTest {
@Test
fun saveAndReadTest() {
studentLocal.saveStudents(listOf(Student(email = "test", password = "test123", schoolSymbol = "23", scrapperBaseUrl = "fakelog.cf", loginType = "AUTO", isCurrent = true, studentName = "", schoolName = "", studentId = 0, classId = 1, symbol = "", registrationDate = now(), className = "", loginMode = "API", certificateKey = "", privateKey = "", mobileBaseUrl = "", userLoginId = 0, isParent = false)))
studentLocal.saveStudents(listOf(Student(email = "test", password = "test123", schoolSymbol = "23", scrapperBaseUrl = "fakelog.cf", loginType = "AUTO", isCurrent = true, studentName = "", schoolShortName = "", schoolName = "", studentId = 0, classId = 1, symbol = "", registrationDate = now(), className = "", loginMode = "API", certificateKey = "", privateKey = "", mobileBaseUrl = "", userLoginId = 0, isParent = false)))
.blockingGet()
val student = studentLocal.getCurrentStudent(true).blockingGet()

View File

@ -21,7 +21,7 @@ fun createTimetableLocal(start: LocalDateTime, number: Int, room: String = "", s
teacher = teacher,
teacherOld = "",
info = "",
studentPlan = true,
isStudentPlan = true,
changes = changes,
canceled = false
)

View File

@ -41,7 +41,7 @@ class TimetableLocalTest {
))
val exams = timetableDb.getTimetable(
Semester(1, 2, "", 1, 1, 2019, true, LocalDate.now(), LocalDate.now(), 1, 1),
Semester(1, 2, "", 1, 1, 2019, LocalDate.now(), LocalDate.now(), 1, 1),
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 14)
).blockingGet()

View File

@ -5,13 +5,12 @@ package io.github.wulkanowy.utils
import android.content.Context
import timber.log.Timber
fun initCrashlytics(context: Context, appInfo: AppInfo) {
// do nothing
fun initCrashlytics(context: Context, appInfo: AppInfo) {}
open class TimberTreeNoOp : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {}
}
class CrashlyticsTree : Timber.Tree() {
class CrashlyticsTree : TimberTreeNoOp()
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
// do nothing
}
}
class CrashlyticsExceptionTree : TimberTreeNoOp()

View File

@ -43,8 +43,8 @@
android:name=".ui.modules.message.send.SendMessageActivity"
android:configChanges="orientation|screenSize"
android:label="@string/send_message_title"
android:windowSoftInputMode="adjustResize"
android:theme="@style/WulkanowyTheme.NoActionBar" />
android:theme="@style/WulkanowyTheme.NoActionBar"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity"
android:excludeFromRecents="true"
@ -96,6 +96,16 @@
android:exported="false"
tools:node="remove" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<meta-data
android:name="io.fabric.ApiKey"
android:value="${fabric_api_key}" />

View File

@ -20,7 +20,7 @@
"githubUsername": "doteq"
},
{
"displayName": "Pavuloff",
"displayName": "Paweł Krzyś",
"githubUsername": "pavuloff"
},
{
@ -30,5 +30,9 @@
{
"displayName": "Dinolek",
"githubUsername": "Dinolek"
},
{
"displayName": "Mateusz Idziejczak",
"githubUsername": "PanTajemnic"
}
]

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy
import android.content.Context
import android.util.Log.DEBUG
import android.util.Log.INFO
import android.util.Log.VERBOSE
import androidx.multidex.MultiDex
@ -11,11 +12,13 @@ import dagger.android.AndroidInjector
import dagger.android.support.DaggerApplication
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.utils.Log
import fr.bipi.tressence.file.FileLoggerTree
import io.github.wulkanowy.di.DaggerAppComponent
import io.github.wulkanowy.services.sync.SyncWorkerFactory
import io.github.wulkanowy.ui.base.ThemeManager
import io.github.wulkanowy.utils.ActivityLifecycleLogger
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.CrashlyticsExceptionTree
import io.github.wulkanowy.utils.CrashlyticsTree
import io.github.wulkanowy.utils.DebugLogTree
import io.github.wulkanowy.utils.initCrashlytics
@ -54,9 +57,17 @@ class WulkanowyApp : DaggerApplication(), Configuration.Provider {
private fun initLogging() {
if (appInfo.isDebug) {
Timber.plant(DebugLogTree())
FlexibleAdapter.enableLogs(Log.Level.DEBUG)
Timber.plant(DebugLogTree())
Timber.plant(FileLoggerTree.Builder()
.withFileName("wulkanowy.%g.log")
.withDirName(applicationContext.filesDir.absolutePath)
.withFileLimit(10)
.withMinPriority(DEBUG)
.build()
)
} else {
Timber.plant(CrashlyticsExceptionTree())
Timber.plant(CrashlyticsTree())
}
registerActivityLifecycleCallbacks(ActivityLifecycleLogger())

View File

@ -62,6 +62,7 @@ import io.github.wulkanowy.data.db.migrations.Migration19
import io.github.wulkanowy.data.db.migrations.Migration2
import io.github.wulkanowy.data.db.migrations.Migration20
import io.github.wulkanowy.data.db.migrations.Migration21
import io.github.wulkanowy.data.db.migrations.Migration22
import io.github.wulkanowy.data.db.migrations.Migration3
import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5
@ -103,7 +104,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 21
const val VERSION_SCHEMA = 22
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> {
return arrayOf(
@ -126,7 +127,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration18(),
Migration19(sharedPrefProvider),
Migration20(),
Migration21()
Migration21(),
Migration22()
)
}

View File

@ -11,7 +11,7 @@ import javax.inject.Singleton
interface GradePointsStatisticsDao : BaseDao<GradePointsStatistics> {
@Query("SELECT * FROM GradesPointsStatistics WHERE student_id = :studentId AND semester_id = :semesterId AND subject = :subjectName")
fun loadSubject(semesterId: Int, studentId: Int, subjectName: String): Maybe<GradePointsStatistics>
fun loadSubject(semesterId: Int, studentId: Int, subjectName: String): Maybe<List<GradePointsStatistics>>
@Query("SELECT * FROM GradesPointsStatistics WHERE student_id = :studentId AND semester_id = :semesterId")
fun loadAll(semesterId: Int, studentId: Int): Maybe<List<GradePointsStatistics>>

View File

@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Message
import io.reactivex.Maybe
import io.reactivex.Single
@Dao
interface MessagesDao : BaseDao<Message> {
@ -12,7 +13,7 @@ interface MessagesDao : BaseDao<Message> {
fun loadAll(studentId: Int, folder: Int): Maybe<List<Message>>
@Query("SELECT * FROM Messages WHERE id = :id")
fun load(id: Long): Maybe<Message>
fun load(id: Long): Single<Message>
@Query("SELECT * FROM Messages WHERE student_id = :studentId AND removed = 1 ORDER BY date DESC")
fun loadDeleted(studentId: Int): Maybe<List<Message>>

View File

@ -27,9 +27,6 @@ data class Semester(
@ColumnInfo(name = "semester_name")
val semesterName: Int,
@ColumnInfo(name = "is_current")
val isCurrent: Boolean,
val start: LocalDate,
val end: LocalDate,
@ -43,4 +40,8 @@ data class Semester(
@PrimaryKey(autoGenerate = true)
var id: Long = 0
@ColumnInfo(name = "is_current")
var current: Boolean = false
}

View File

@ -49,6 +49,9 @@ data class Student(
@ColumnInfo(name = "school_id")
val schoolSymbol: String,
@ColumnInfo(name ="school_short")
val schoolShortName: String,
@ColumnInfo(name = "school_name")
val schoolName: String,

View File

@ -41,7 +41,7 @@ data class Timetable(
val info: String,
@ColumnInfo(name = "student_plan")
val studentPlan: Boolean,
val isStudentPlan: Boolean,
val changes: Boolean,

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration22 : Migration(21, 22) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Students ADD COLUMN school_short TEXT NOT NULL DEFAULT ''")
}
}

View File

@ -0,0 +1,14 @@
package io.github.wulkanowy.data.pojos
import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.ui.modules.grade.statistics.ViewType
data class GradeStatisticsItem(
val type: ViewType,
val partial: List<GradeStatistics>,
val points: GradePointsStatistics?
)

View File

@ -12,7 +12,7 @@ class AppCreatorRepository @Inject constructor(private val assets: AssetManager)
fun getAppCreators(): Single<List<AppCreator>> {
return Single.fromCallable<List<AppCreator>> {
Gson().fromJson(
assets.open("creators.json").bufferedReader().use { it.readText() },
assets.open("contributors.json").bufferedReader().use { it.readText() },
Array<AppCreator>::class.java
).toList()
}

View File

@ -5,7 +5,6 @@ import io.github.wulkanowy.data.db.dao.GradeStatisticsDao
import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.utils.roundToDecimalPlaces
import io.reactivex.Maybe
import javax.inject.Inject
import javax.inject.Singleton
@ -30,23 +29,17 @@ class GradeStatisticsLocal @Inject constructor(
list.groupBy { it.grade }.map {
GradeStatistics(semester.studentId, semester.semesterId, subjectName, it.key,
it.value.fold(0) { acc, e -> acc + e.amount }, false)
}
} + list
}
else -> gradeStatisticsDb.loadSubject(semester.semesterId, semester.studentId, subjectName, isSemester)
}.filter { it.isNotEmpty() }
}
fun getGradesPointsStatistics(semester: Semester, subjectName: String): Maybe<GradePointsStatistics> {
fun getGradesPointsStatistics(semester: Semester, subjectName: String): Maybe<List<GradePointsStatistics>> {
return when (subjectName) {
"Wszystkie" -> gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId).flatMap { list ->
if (list.isEmpty()) return@flatMap Maybe.empty<GradePointsStatistics>()
Maybe.just(GradePointsStatistics(semester.studentId, semester.semesterId, subjectName,
(list.fold(.0) { acc, e -> acc + e.others } / list.size).roundToDecimalPlaces(2),
(list.fold(.0) { acc, e -> acc + e.student } / list.size).roundToDecimalPlaces(2)
))
}
"Wszystkie" -> gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId)
else -> gradePointsStatisticsDb.loadSubject(semester.semesterId, semester.studentId, subjectName)
}
}.filter { it.isNotEmpty() }
}
fun saveGradesStatistics(gradesStatistics: List<GradeStatistics>) {

View File

@ -5,8 +5,9 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.ui.modules.grade.statistics.ViewType
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Maybe
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
@ -19,8 +20,8 @@ class GradeStatisticsRepository @Inject constructor(
private val remote: GradeStatisticsRemote
) {
fun getGradesStatistics(semester: Semester, subjectName: String, isSemester: Boolean, forceRefresh: Boolean = false): Single<List<GradeStatistics>> {
return local.getGradesStatistics(semester, isSemester, subjectName).filter { !forceRefresh }
fun getGradesStatistics(semester: Semester, subjectName: String, isSemester: Boolean, forceRefresh: Boolean = false): Single<List<GradeStatisticsItem>> {
return local.getGradesStatistics(semester, isSemester, subjectName).map { it.mapToStatisticItems() }.filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getGradeStatistics(semester, isSemester)
@ -31,21 +32,43 @@ class GradeStatisticsRepository @Inject constructor(
local.deleteGradesStatistics(old.uniqueSubtract(new))
local.saveGradesStatistics(new.uniqueSubtract(old))
}
}.flatMap { local.getGradesStatistics(semester, isSemester, subjectName).toSingle(emptyList()) })
}.flatMap { local.getGradesStatistics(semester, isSemester, subjectName).map { it.mapToStatisticItems() }.toSingle(emptyList()) })
}
fun getGradesPointsStatistics(semester: Semester, subjectName: String, forceRefresh: Boolean): Maybe<GradePointsStatistics> {
return local.getGradesPointsStatistics(semester, subjectName).filter { !forceRefresh }
fun getGradesPointsStatistics(semester: Semester, subjectName: String, forceRefresh: Boolean): Single<List<GradeStatisticsItem>> {
return local.getGradesPointsStatistics(semester, subjectName).map { it.mapToStatisticsItem() }.filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMapMaybe {
if (it) remote.getGradePointsStatistics(semester).toMaybe()
else Maybe.error(UnknownHostException())
.flatMap {
if (it) remote.getGradePointsStatistics(semester)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getGradesPointsStatistics(semester).defaultIfEmpty(emptyList())
local.getGradesPointsStatistics(semester).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteGradesPointsStatistics(old.uniqueSubtract(new))
local.saveGradesPointsStatistics(new.uniqueSubtract(old))
}
}.flatMap { local.getGradesPointsStatistics(semester, subjectName) })
}.flatMap { local.getGradesPointsStatistics(semester, subjectName).map { it.mapToStatisticsItem() }.toSingle(emptyList()) })
}
private fun List<GradeStatistics>.mapToStatisticItems(): List<GradeStatisticsItem> {
return groupBy { it.subject }.map {
GradeStatisticsItem(
type = ViewType.PARTIAL,
partial = it.value
.sortedByDescending { item -> item.grade }
.filter { item -> item.amount != 0 },
points = null
)
}
}
private fun List<GradePointsStatistics>.mapToStatisticsItem(): List<GradeStatisticsItem> {
return map {
GradeStatisticsItem(
type = ViewType.POINTS,
partial = emptyList(),
points = it
)
}
}
}

View File

@ -0,0 +1,39 @@
package io.github.wulkanowy.data.repositories.logger
import android.content.Context
import io.reactivex.Single
import java.io.File
import java.io.FileNotFoundException
import javax.inject.Inject
class LoggerRepository @Inject constructor(private val context: Context) {
fun getLastLogLines(): Single<List<String>> {
return getLastModified()
.map { it.readText() }
.map { it.split("\n") }
}
fun getLogFiles(): Single<List<File>> {
return Single.fromCallable {
File(context.filesDir.absolutePath).listFiles(File::isFile)?.filter {
it.name.endsWith(".log")
}
}
}
private fun getLastModified(): Single<File> {
return Single.fromCallable {
var lastModifiedTime = Long.MIN_VALUE
var chosenFile: File? = null
File(context.filesDir.absolutePath).listFiles(File::isFile)?.forEach { file ->
if (file.lastModified() > lastModifiedTime) {
lastModifiedTime = file.lastModified()
chosenFile = file
}
}
if (chosenFile == null) throw FileNotFoundException("Log file not found")
chosenFile
}
}
}

View File

@ -2,7 +2,7 @@ package io.github.wulkanowy.data.repositories.luckynumber
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.reactivex.Maybe
import org.threeten.bp.LocalDate
import javax.inject.Inject
@ -23,7 +23,7 @@ class LuckyNumberLocal @Inject constructor(private val luckyNumberDb: LuckyNumbe
luckyNumberDb.deleteAll(listOf(luckyNumber))
}
fun getLuckyNumber(semester: Semester, date: LocalDate): Maybe<LuckyNumber> {
return luckyNumberDb.load(semester.studentId, date)
fun getLuckyNumber(student: Student, date: LocalDate): Maybe<LuckyNumber> {
return luckyNumberDb.load(student.studentId, date)
}
}

View File

@ -1,7 +1,7 @@
package io.github.wulkanowy.data.repositories.luckynumber
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.Sdk
import io.reactivex.Maybe
import org.threeten.bp.LocalDate
@ -11,14 +11,13 @@ import javax.inject.Singleton
@Singleton
class LuckyNumberRemote @Inject constructor(private val sdk: Sdk) {
fun getLuckyNumber(semester: Semester): Maybe<LuckyNumber> {
return sdk.getLuckyNumber()
.map {
LuckyNumber(
studentId = semester.studentId,
date = LocalDate.now(),
luckyNumber = it
)
}
fun getLuckyNumber(student: Student): Maybe<LuckyNumber> {
return sdk.getLuckyNumber(student.schoolShortName).map {
LuckyNumber(
studentId = student.studentId,
date = LocalDate.now(),
luckyNumber = it
)
}
}
}

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.data.repositories.luckynumber
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.reactivex.Completable
import io.reactivex.Maybe
import org.threeten.bp.LocalDate
@ -18,14 +18,14 @@ class LuckyNumberRepository @Inject constructor(
private val remote: LuckyNumberRemote
) {
fun getLuckyNumber(semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Maybe<LuckyNumber> {
return local.getLuckyNumber(semester, LocalDate.now()).filter { !forceRefresh }
fun getLuckyNumber(student: Student, forceRefresh: Boolean = false, notify: Boolean = false): Maybe<LuckyNumber> {
return local.getLuckyNumber(student, LocalDate.now()).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMapMaybe {
if (it) remote.getLuckyNumber(semester)
if (it) remote.getLuckyNumber(student)
else Maybe.error(UnknownHostException())
}.flatMap { new ->
local.getLuckyNumber(semester, LocalDate.now())
local.getLuckyNumber(student, LocalDate.now())
.doOnSuccess { old ->
if (new != old) {
local.deleteLuckyNumber(old)
@ -39,13 +39,13 @@ class LuckyNumberRepository @Inject constructor(
if (notify) isNotified = false
})
}
}.flatMap({ local.getLuckyNumber(semester, LocalDate.now()) }, { Maybe.error(it) },
{ local.getLuckyNumber(semester, LocalDate.now()) })
}.flatMap({ local.getLuckyNumber(student, LocalDate.now()) }, { Maybe.error(it) },
{ local.getLuckyNumber(student, LocalDate.now()) })
)
}
fun getNotNotifiedLuckyNumber(semester: Semester): Maybe<LuckyNumber> {
return local.getLuckyNumber(semester, LocalDate.now()).filter { !it.isNotified }
fun getNotNotifiedLuckyNumber(student: Student): Maybe<LuckyNumber> {
return local.getLuckyNumber(student, LocalDate.now()).filter { !it.isNotified }
}
fun updateLuckyNumber(luckyNumber: LuckyNumber): Completable {

View File

@ -5,6 +5,7 @@ import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.message.MessageFolder.TRASHED
import io.reactivex.Maybe
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@ -23,7 +24,7 @@ class MessageLocal @Inject constructor(private val messagesDb: MessagesDao) {
messagesDb.deleteAll(messages)
}
fun getMessage(id: Long): Maybe<Message> {
fun getMessage(id: Long): Single<Message> {
return messagesDb.load(id)
}

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Completable
import io.reactivex.Maybe
import io.reactivex.Single
import timber.log.Timber
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@ -51,21 +52,26 @@ class MessageRepository @Inject constructor(
return Single.just(sdkHelper.init(student))
.flatMap { _ ->
local.getMessage(messageDbId)
.filter { it.content.isNotEmpty() }
.filter {
it.content.isNotEmpty().also { status ->
Timber.d("Message content in db empty: ${!status}")
}
}
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) local.getMessage(messageDbId).toSingle()
if (it) local.getMessage(messageDbId)
else Single.error(UnknownHostException())
}
.flatMap { dbMessage ->
remote.getMessagesContent(dbMessage, markAsRead).doOnSuccess {
local.updateMessages(listOf(dbMessage.copy(unread = false).apply {
local.updateMessages(listOf(dbMessage.copy(unread = !markAsRead).apply {
id = dbMessage.id
content = content.ifBlank { it }
}))
Timber.d("Message $messageDbId with blank content: ${dbMessage.content.isBlank()}, marked as read")
}
}.flatMap {
local.getMessage(messageDbId).toSingle()
local.getMessage(messageDbId)
}
)
}

View File

@ -0,0 +1,19 @@
package io.github.wulkanowy.data.repositories.recover
import io.github.wulkanowy.sdk.Sdk
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecoverRemote @Inject constructor(private val sdk: Sdk) {
fun getReCaptchaSiteKey(host: String, symbol: String): Single<Pair<String, String>> {
return sdk.getPasswordResetCaptchaCode(host, symbol)
}
fun sendRecoverRequest(url: String, symbol: String, email: String, reCaptchaResponse: String): Single<String> {
return sdk.sendPasswordResetRequest(url, symbol, email, reCaptchaResponse)
}
}

View File

@ -0,0 +1,26 @@
package io.github.wulkanowy.data.repositories.recover
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecoverRepository @Inject constructor(private val settings: InternetObservingSettings, private val remote: RecoverRemote) {
fun getReCaptchaSiteKey(host: String, symbol: String): Single<Pair<String, String>> {
return ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getReCaptchaSiteKey(host, symbol)
else Single.error(UnknownHostException())
}
}
fun sendRecoverRequest(url: String, symbol: String, email: String, reCaptchaResponse: String): Single<String> {
return ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.sendRecoverRequest(url, symbol, email, reCaptchaResponse)
else Single.error(UnknownHostException())
}
}
}

View File

@ -19,6 +19,6 @@ class SemesterLocal @Inject constructor(private val semesterDb: SemesterDao) {
}
fun getSemesters(student: Student): Maybe<List<Semester>> {
return semesterDb.loadAll(student.studentId, student.classId).filter { !it.isEmpty() }
return semesterDb.loadAll(student.studentId, student.classId).filter { it.isNotEmpty() }
}
}

View File

@ -20,7 +20,6 @@ class SemesterRemote @Inject constructor(private val sdk: Sdk) {
schoolYear = it.schoolYear,
semesterId = it.semesterId,
semesterName = it.semesterNumber,
isCurrent = it.current,
start = it.start,
end = it.end,
classId = it.classId,

View File

@ -5,10 +5,11 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.SdkHelper
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.getCurrentOrLast
import io.github.wulkanowy.utils.isCurrent
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Maybe
import io.reactivex.Single
import timber.log.Timber
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@ -21,28 +22,30 @@ class SemesterRepository @Inject constructor(
private val sdkHelper: SdkHelper
) {
fun getSemesters(student: Student, forceRefresh: Boolean = false): Single<List<Semester>> {
fun getSemesters(student: Student, forceRefresh: Boolean = false, refreshOnNoCurrent: Boolean = false): Single<List<Semester>> {
return Maybe.just(sdkHelper.init(student))
.flatMap { local.getSemesters(student).filter { !forceRefresh } }
.flatMap {
local.getSemesters(student).filter { !forceRefresh }.filter {
if (refreshOnNoCurrent) {
it.any { semester -> semester.isCurrent }
} else true
}
}
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getSemesters(student) else Single.error(UnknownHostException())
if (it) remote.getSemesters(student)
else Single.error(UnknownHostException())
}.flatMap { new ->
val currentSemesters = new.filter { it.isCurrent }
if (currentSemesters.size == 1) {
local.getSemesters(student).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteSemesters(old.uniqueSubtract(new))
local.saveSemesters(new.uniqueSubtract(old))
}
} else {
Timber.i("Current semesters list:\n${new.joinToString(separator = "\n")}")
throw IllegalArgumentException("Current semester can be only one.")
if (new.isEmpty()) throw IllegalArgumentException("Empty semester list!")
local.getSemesters(student).toSingle(emptyList()).doOnSuccess { old ->
local.deleteSemesters(old.uniqueSubtract(new))
local.saveSemesters(new.uniqueSubtract(old))
}
}.flatMap { local.getSemesters(student).toSingle(emptyList()) })
}
fun getCurrentSemester(student: Student, forceRefresh: Boolean = false): Single<Semester> {
return getSemesters(student, forceRefresh).map { item -> item.single { it.isCurrent } }
return getSemesters(student, forceRefresh).map { it.getCurrentOrLast() }
}
}

View File

@ -22,6 +22,7 @@ class StudentRemote @Inject constructor(private val sdk: Sdk) {
userLoginId = student.userLoginId,
studentName = student.studentName,
schoolSymbol = student.schoolSymbol,
schoolShortName = student.schoolShortName,
schoolName = student.schoolName,
className = student.className,
classId = student.classId,

View File

@ -29,7 +29,7 @@ class StudentRepository @Inject constructor(
}
}
fun getStudentsScrapper(email: String, password: String, endpoint: String, symbol: String = ""): Single<List<Student>> {
fun getStudentsScrapper(email: String, password: String, endpoint: String, symbol: String): Single<List<Student>> {
return ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getStudentsScrapper(email, password, endpoint, symbol)
else Single.error(UnknownHostException("No internet connection"))

View File

@ -30,7 +30,7 @@ class TimetableRemote @Inject constructor(private val sdk: Sdk) {
teacher = it.teacher,
teacherOld = it.teacherOld,
info = it.info,
studentPlan = it.studentPlan,
isStudentPlan = it.studentPlan,
changes = it.changes,
canceled = it.canceled
)

View File

@ -9,6 +9,12 @@ import dagger.Module
import dagger.Provides
import dagger.android.ContributesAndroidInjector
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.NewGradesChannel
import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork
import io.github.wulkanowy.services.sync.works.AttendanceWork
import io.github.wulkanowy.services.sync.works.CompletedLessonWork
@ -29,11 +35,10 @@ import javax.inject.Singleton
@Suppress("unused")
@AssistedModule
@Module(includes = [AssistedInject_ServicesModule::class, ServicesModule.Static::class])
@Module(includes = [AssistedInject_ServicesModule::class])
abstract class ServicesModule {
@Module
object Static {
companion object {
@Provides
fun provideWorkManager(context: Context) = WorkManager.getInstance(context)
@ -101,4 +106,24 @@ abstract class ServicesModule {
@Binds
@IntoSet
abstract fun provideGradeStatistics(work: GradeStatisticsWork): Work
@Binds
@IntoSet
abstract fun provideDebugChannel(channel: DebugChannel): Channel
@Binds
@IntoSet
abstract fun provideLuckyNumberChannel(channel: LuckyNumberChannel): Channel
@Binds
@IntoSet
abstract fun provideNewGradesChannel(channel: NewGradesChannel): Channel
@Binds
@IntoSet
abstract fun provideNewMessageChannel(channel: NewMessagesChannel): Channel
@Binds
@IntoSet
abstract fun provideNewNotesChannel(channel: NewNotesChannel): Channel
}

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.services.sync
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O
import androidx.core.app.NotificationManagerCompat
import androidx.work.BackoffPolicy.EXPONENTIAL
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy.KEEP
@ -13,8 +14,7 @@ import androidx.work.WorkManager
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.SharedPrefProvider.Companion.APP_VERSION_CODE_KEY
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.channels.NewEntriesChannel
import io.github.wulkanowy.services.sync.channels.Channel
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.isHolidays
import org.threeten.bp.LocalDate.now
@ -27,9 +27,9 @@ import javax.inject.Singleton
class SyncManager @Inject constructor(
private val workManager: WorkManager,
private val preferencesRepository: PreferencesRepository,
channels: Set<@JvmSuppressWildcards Channel>,
notificationManager: NotificationManagerCompat,
sharedPrefProvider: SharedPrefProvider,
newEntriesChannel: NewEntriesChannel,
debugChannel: DebugChannel,
appInfo: AppInfo
) {
@ -37,8 +37,8 @@ class SyncManager @Inject constructor(
if (now().isHolidays) stopSyncWorker()
if (SDK_INT > O) {
newEntriesChannel.create()
if (appInfo.isDebug) debugChannel.create()
channels.forEach { it.create() }
notificationManager.deleteNotificationChannel("new_entries_channel")
}
if (sharedPrefProvider.getLong(APP_VERSION_CODE_KEY, -1L) != appInfo.versionCode.toLong()) {

View File

@ -0,0 +1,6 @@
package io.github.wulkanowy.services.sync.channels
interface Channel {
fun create()
}

View File

@ -7,19 +7,22 @@ import android.app.NotificationManager.IMPORTANCE_DEFAULT
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import io.github.wulkanowy.R
import io.github.wulkanowy.utils.AppInfo
import javax.inject.Inject
@TargetApi(26)
class DebugChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat,
private val context: Context
) {
private val context: Context,
private val appInfo: AppInfo
) : Channel {
companion object {
const val CHANNEL_ID = "debug_channel"
}
fun create() {
override fun create() {
if (appInfo.isDebug) return
notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_debug), IMPORTANCE_DEFAULT)
.apply {

View File

@ -0,0 +1,31 @@
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 io.github.wulkanowy.R
import javax.inject.Inject
@TargetApi(26)
class LuckyNumberChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat,
private val context: Context
) : Channel {
companion object {
const val CHANNEL_ID = "lucky_number_channel"
}
override fun create() {
notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_lucky_number), NotificationManager.IMPORTANCE_HIGH)
.apply {
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
})
}
}

View File

@ -0,0 +1,31 @@
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 io.github.wulkanowy.R
import javax.inject.Inject
@TargetApi(26)
class NewGradesChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat,
private val context: Context
) : Channel {
companion object {
const val CHANNEL_ID = "new_grade_channel"
}
override fun create() {
notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_new_grades), NotificationManager.IMPORTANCE_HIGH)
.apply {
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
})
}
}

View File

@ -0,0 +1,31 @@
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 io.github.wulkanowy.R
import javax.inject.Inject
@TargetApi(26)
class NewMessagesChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat,
private val context: Context
) : Channel {
companion object {
const val CHANNEL_ID = "new_message_channel"
}
override fun create() {
notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_new_message), NotificationManager.IMPORTANCE_HIGH)
.apply {
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
})
}
}

View File

@ -1,31 +1,31 @@
package io.github.wulkanowy.services.sync.channels
import android.annotation.TargetApi
import android.app.Notification.VISIBILITY_PUBLIC
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager.IMPORTANCE_HIGH
import android.app.NotificationManager
import android.content.Context
import androidx.core.app.NotificationManagerCompat
import io.github.wulkanowy.R
import javax.inject.Inject
@TargetApi(26)
class NewEntriesChannel @Inject constructor(
class NewNotesChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat,
private val context: Context
) {
) : Channel {
companion object {
const val CHANNEL_ID = "new_entries_channel"
const val CHANNEL_ID = "new_notes_channel"
}
fun create() {
override fun create() {
notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_new_entries), IMPORTANCE_HIGH)
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_new_notes), NotificationManager.IMPORTANCE_HIGH)
.apply {
enableLights(true)
enableVibration(true)
lockscreenVisibility = VISIBILITY_PUBLIC
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
})
}
}

View File

@ -13,7 +13,7 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.grade.GradeRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.NewEntriesChannel
import io.github.wulkanowy.services.sync.channels.NewGradesChannel
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getCompatColor
@ -38,7 +38,7 @@ class GradeWork @Inject constructor(
}
private fun notify(grades: List<Grade>) {
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(context, NewEntriesChannel.CHANNEL_ID)
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(context, NewGradesChannel.CHANNEL_ID)
.setContentTitle(context.resources.getQuantityString(R.plurals.grade_new_items, grades.size, grades.size))
.setContentText(context.resources.getQuantityString(R.plurals.grade_notify_new_items, grades.size, grades.size))
.setSmallIcon(R.drawable.ic_stat_grade)

View File

@ -13,7 +13,7 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.luckynumber.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.NewEntriesChannel
import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getCompatColor
@ -29,8 +29,8 @@ class LuckyNumberWork @Inject constructor(
) : Work {
override fun create(student: Student, semester: Semester): Completable {
return luckyNumberRepository.getLuckyNumber(semester, true, preferencesRepository.isNotificationsEnable)
.flatMap { luckyNumberRepository.getNotNotifiedLuckyNumber(semester) }
return luckyNumberRepository.getLuckyNumber(student, true, preferencesRepository.isNotificationsEnable)
.flatMap { luckyNumberRepository.getNotNotifiedLuckyNumber(student) }
.flatMapCompletable {
notify(it)
luckyNumberRepository.updateLuckyNumber(it.apply { isNotified = true })
@ -38,7 +38,7 @@ class LuckyNumberWork @Inject constructor(
}
private fun notify(luckyNumber: LuckyNumber) {
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(context, NewEntriesChannel.CHANNEL_ID)
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(context, LuckyNumberChannel.CHANNEL_ID)
.setContentTitle(context.getString(R.string.lucky_number_notify_new_item_title))
.setContentText(context.getString(R.string.lucky_number_notify_new_item, luckyNumber.luckyNumber))
.setSmallIcon(R.drawable.ic_stat_luckynumber)

View File

@ -14,7 +14,7 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.message.MessageFolder.RECEIVED
import io.github.wulkanowy.data.repositories.message.MessageRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.NewEntriesChannel
import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getCompatColor
@ -39,7 +39,7 @@ class MessageWork @Inject constructor(
}
private fun notify(messages: List<Message>) {
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(context, NewEntriesChannel.CHANNEL_ID)
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(context, NewMessagesChannel.CHANNEL_ID)
.setContentTitle(context.resources.getQuantityString(R.plurals.message_new_items, messages.size, messages.size))
.setContentText(context.resources.getQuantityString(R.plurals.message_notify_new_items, messages.size, messages.size))
.setSmallIcon(R.drawable.ic_stat_message)

View File

@ -13,7 +13,7 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.note.NoteRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.NewEntriesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getCompatColor
@ -38,7 +38,7 @@ class NoteWork @Inject constructor(
}
private fun notify(notes: List<Note>) {
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(context, NewEntriesChannel.CHANNEL_ID)
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(context, NewNotesChannel.CHANNEL_ID)
.setContentTitle(context.resources.getQuantityString(R.plurals.note_new_items, notes.size, notes.size))
.setContentText(context.resources.getQuantityString(R.plurals.note_notify_new_items, notes.size, notes.size))
.setSmallIcon(R.drawable.ic_stat_note)

View File

@ -4,6 +4,7 @@ import android.content.Intent
import android.widget.RemoteViewsService
import dagger.android.AndroidInjection
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.data.repositories.timetable.TimetableRepository
@ -22,6 +23,9 @@ class TimetableWidgetService : RemoteViewsService() {
@Inject
lateinit var semesterRepo: SemesterRepository
@Inject
lateinit var prefRepository: PreferencesRepository
@Inject
lateinit var sharedPref: SharedPrefProvider
@ -30,6 +34,6 @@ class TimetableWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
AndroidInjection.inject(this)
return TimetableWidgetFactory(timetableRepo, studentRepo, semesterRepo, sharedPref, schedulers, applicationContext, intent)
return TimetableWidgetFactory(timetableRepo, studentRepo, semesterRepo, prefRepository, sharedPref, schedulers, applicationContext, intent)
}
}

View File

@ -16,8 +16,9 @@ import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.about.creator.CreatorFragment
import io.github.wulkanowy.ui.modules.about.contributor.ContributorFragment
import io.github.wulkanowy.ui.modules.about.license.LicenseFragment
import io.github.wulkanowy.ui.modules.about.logviewer.LogViewerFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
@ -45,7 +46,7 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
override val creatorsRes: Triple<String, String, Drawable?>?
get() = context?.run {
Triple(getString(R.string.about_creator), getString(R.string.about_creator_summary), getCompatDrawable(R.drawable.ic_about_creator))
Triple(getString(R.string.about_contributor), getString(R.string.about_contributor_summary), getCompatDrawable(R.drawable.ic_about_creator))
}
override val feedbackRes: Triple<String, String, Drawable?>?
@ -110,6 +111,10 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
}
}
override fun openLogViewer() {
if (appInfo.isDebug) (activity as? MainActivity)?.pushView(LogViewerFragment.newInstance())
}
override fun openDiscordInvite() {
context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage)
}
@ -150,7 +155,7 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
}
override fun openCreators() {
(activity as? MainActivity)?.pushView(CreatorFragment.newInstance())
(activity as? MainActivity)?.pushView(ContributorFragment.newInstance())
}
override fun openPrivacyPolicy() {

View File

@ -27,6 +27,11 @@ class AboutPresenter @Inject constructor(
if (item !is AboutItem) return
view?.run {
when (item.title) {
versionRes?.first -> {
Timber.i("Opening log viewer")
openLogViewer()
analytics.logEvent("about_open", "name" to "log_viewer")
}
feedbackRes?.first -> {
Timber.i("Opening email client")
openEmailClient()

View File

@ -25,6 +25,8 @@ interface AboutView : BaseView {
fun updateData(header: AboutScrollableHeader, items: List<AboutItem>)
fun openLogViewer()
fun openDiscordInvite()
fun openEmailClient()

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.ui.modules.about.creator
package io.github.wulkanowy.ui.modules.about.contributor
import android.os.Bundle
import android.view.LayoutInflater
@ -18,18 +18,18 @@ import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_creator.*
import javax.inject.Inject
class CreatorFragment : BaseFragment(), CreatorView, MainView.TitledView {
class ContributorFragment : BaseFragment(), ContributorView, MainView.TitledView {
@Inject
lateinit var presenter: CreatorPresenter
lateinit var presenter: ContributorPresenter
@Inject
lateinit var creatorsAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
override val titleStringId get() = R.string.creators_title
override val titleStringId get() = R.string.contributors_title
companion object {
fun newInstance() = CreatorFragment()
fun newInstance() = ContributorFragment()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -53,7 +53,7 @@ class CreatorFragment : BaseFragment(), CreatorView, MainView.TitledView {
creatorSeeMore.setOnClickListener { presenter.onSeeMoreClick() }
}
override fun updateData(data: List<CreatorItem>) {
override fun updateData(data: List<ContributorItem>) {
creatorsAdapter.updateDataSet(data)
}

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.ui.modules.about.creator
package io.github.wulkanowy.ui.modules.about.contributor
import android.view.View
import coil.api.load
@ -10,11 +10,12 @@ import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import io.github.wulkanowy.data.pojos.AppCreator
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_creator.*
import kotlinx.android.synthetic.main.item_contributor.*
class CreatorItem(val creator: AppCreator) : AbstractFlexibleItem<CreatorItem.ViewHolder>() {
class ContributorItem(val creator: AppCreator) :
AbstractFlexibleItem<ContributorItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_creator
override fun getLayoutRes() = R.layout.item_contributor
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>) = ViewHolder(view, adapter)
@ -33,7 +34,7 @@ class CreatorItem(val creator: AppCreator) : AbstractFlexibleItem<CreatorItem.Vi
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CreatorItem
other as ContributorItem
if (creator != other.creator) return false

View File

@ -1,30 +1,28 @@
package io.github.wulkanowy.ui.modules.about.creator
package io.github.wulkanowy.ui.modules.about.contributor
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.pojos.AppCreator
import io.github.wulkanowy.data.repositories.appcreator.AppCreatorRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider
import io.reactivex.Single
import javax.inject.Inject
class CreatorPresenter @Inject constructor(
class ContributorPresenter @Inject constructor(
schedulers: SchedulersProvider,
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val appCreatorRepository: AppCreatorRepository
) : BasePresenter<CreatorView>(errorHandler, studentRepository, schedulers) {
) : BasePresenter<ContributorView>(errorHandler, studentRepository, schedulers) {
override fun onAttachView(view: CreatorView) {
override fun onAttachView(view: ContributorView) {
super.onAttachView(view)
view.initView()
loadData()
}
fun onItemSelected(item: AbstractFlexibleItem<*>) {
if (item !is CreatorItem) return
if (item !is ContributorItem) return
view?.openUserGithubPage(item.creator.githubUsername)
}
@ -34,7 +32,7 @@ class CreatorPresenter @Inject constructor(
private fun loadData() {
disposable.add(appCreatorRepository.getAppCreators()
.map { it.map { creator -> CreatorItem(creator) } }
.map { it.map { creator -> ContributorItem(creator) } }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally { view?.showProgress(false) }

View File

@ -1,12 +1,12 @@
package io.github.wulkanowy.ui.modules.about.creator
package io.github.wulkanowy.ui.modules.about.contributor
import io.github.wulkanowy.ui.base.BaseView
interface CreatorView : BaseView {
interface ContributorView : BaseView {
fun initView()
fun updateData(data: List<CreatorItem>)
fun updateData(data: List<ContributorItem>)
fun openUserGithubPage(username: String)

View File

@ -0,0 +1,22 @@
package io.github.wulkanowy.ui.modules.about.logviewer
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class LogViewerAdapter : RecyclerView.Adapter<LogViewerAdapter.ViewHolder>() {
var lines = emptyList<String>()
class ViewHolder(val textView: TextView) : RecyclerView.ViewHolder(textView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(TextView(parent.context))
}
override fun getItemCount() = lines.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = lines[position]
}
}

View File

@ -0,0 +1,98 @@
package io.github.wulkanowy.ui.modules.about.logviewer
import android.content.Intent
import android.content.Intent.EXTRA_EMAIL
import android.content.Intent.EXTRA_STREAM
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.content.FileProvider
import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.BuildConfig.APPLICATION_ID
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import kotlinx.android.synthetic.main.fragment_logviewer.*
import java.io.File
import javax.inject.Inject
class LogViewerFragment : BaseFragment(), LogViewerView, MainView.TitledView {
@Inject
lateinit var presenter: LogViewerPresenter
private val logAdapter = LogViewerAdapter()
override val titleStringId: Int
get() = R.string.logviewer_title
companion object {
fun newInstance() = LogViewerFragment()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_logviewer, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = logViewerRecycler
presenter.onAttachView(this)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.action_menu_logviewer, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.logViewerMenuShare) presenter.onShareLogsSelected()
else false
}
override fun initView() {
with(logViewerRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = logAdapter
}
logViewRefreshButton.setOnClickListener { presenter.onRefreshClick() }
}
override fun setLines(lines: List<String>) {
logAdapter.lines = lines
logAdapter.notifyDataSetChanged()
logViewerRecycler.scrollToPosition(lines.size - 1)
}
override fun shareLogs(files: List<File>) {
val intent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
type = "text/plain"
putExtra(EXTRA_EMAIL, arrayOf("wulkanowyinc@gmail.com"))
addFlags(FLAG_GRANT_READ_URI_PERMISSION)
putParcelableArrayListExtra(EXTRA_STREAM, ArrayList(files.map {
if (SDK_INT < LOLLIPOP) Uri.fromFile(it)
else FileProvider.getUriForFile(requireContext(), "$APPLICATION_ID.fileprovider", it)
}))
}
startActivity(Intent.createChooser(intent, getString(R.string.logviewer_share)))
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,54 @@
package io.github.wulkanowy.ui.modules.about.logviewer
import io.github.wulkanowy.data.repositories.logger.LoggerRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider
import timber.log.Timber
import javax.inject.Inject
class LogViewerPresenter @Inject constructor(
schedulers: SchedulersProvider,
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val loggerRepository: LoggerRepository
) : BasePresenter<LogViewerView>(errorHandler, studentRepository, schedulers) {
override fun onAttachView(view: LogViewerView) {
super.onAttachView(view)
view.initView()
loadLogFile()
}
fun onShareLogsSelected(): Boolean {
disposable.add(loggerRepository.getLogFiles()
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
Timber.i("Loading logs files result: ${it.joinToString { it.name }}")
view?.shareLogs(it)
}, {
Timber.i("Loading logs files result: An exception occurred")
errorHandler.dispatch(it)
}))
return true
}
fun onRefreshClick() {
loadLogFile()
}
private fun loadLogFile() {
disposable.add(loggerRepository.getLastLogLines()
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
Timber.i("Loading last log file result: load ${it.size} lines")
view?.setLines(it)
}, {
Timber.i("Loading last log file result: An exception occurred")
errorHandler.dispatch(it)
}))
}
}

View File

@ -0,0 +1,13 @@
package io.github.wulkanowy.ui.modules.about.logviewer
import io.github.wulkanowy.ui.base.BaseView
import java.io.File
interface LogViewerView : BaseView {
fun initView()
fun setLines(lines: List<String>)
fun shareLogs(files: List<File>)
}

View File

@ -36,6 +36,8 @@ class AttendanceSummaryFragment : BaseFragment(), AttendanceSummaryView, MainVie
fun newInstance() = AttendanceSummaryFragment()
}
override val totalString get() = getString(R.string.attendance_summary_total)
override val titleStringId get() = R.string.attendance_title
override val isViewEmpty get() = attendanceSummaryAdapter.isEmpty

View File

@ -12,6 +12,7 @@ import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.getFormattedName
import org.threeten.bp.Month
import timber.log.Timber
import java.lang.String.format
import java.util.Locale.FRANCE
@ -144,8 +145,25 @@ class AttendanceSummaryPresenter @Inject constructor(
)
}
private fun createAttendanceSummaryTotalItem(attendanceSummary: List<AttendanceSummary>): AttendanceSummaryItem {
return AttendanceSummaryItem(
month = view?.totalString.orEmpty(),
percentage = formatPercentage(attendanceSummary.calculatePercentage()),
present = attendanceSummary.sumBy { it.presence }.toString(),
absence = attendanceSummary.sumBy { it.absence }.toString(),
excusedAbsence = attendanceSummary.sumBy { it.absenceExcused }.toString(),
schoolAbsence = attendanceSummary.sumBy { it.absenceForSchoolReasons }.toString(),
exemption = attendanceSummary.sumBy { it.exemption }.toString(),
lateness = attendanceSummary.sumBy { it.lateness }.toString(),
excusedLateness = attendanceSummary.sumBy { it.latenessExcused }.toString()
)
}
private fun createAttendanceSummaryItems(attendanceSummary: List<AttendanceSummary>): List<AttendanceSummaryItem> {
return attendanceSummary.sortedByDescending { it.id }.map {
if (attendanceSummary.isEmpty()) return emptyList()
return listOf(createAttendanceSummaryTotalItem(attendanceSummary)) + attendanceSummary.sortedByDescending {
if (it.month.value <= Month.JUNE.value) it.month.value + 12 else it.month.value
}.map {
AttendanceSummaryItem(
month = it.month.getFormattedName(),
percentage = formatPercentage(it.calculatePercentage()),

View File

@ -4,6 +4,8 @@ import io.github.wulkanowy.ui.base.BaseView
interface AttendanceSummaryView : BaseView {
val totalString: String
val isViewEmpty: Boolean
fun initView()

View File

@ -40,6 +40,8 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie
override val titleStringId get() = R.string.grade_title
override var subtitleString = ""
override val currentPageIndex get() = gradeViewPager.currentItem
override fun onCreate(savedInstanceState: Bundle?) {
@ -133,6 +135,11 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie
.show()
}
override fun setCurrentSemesterName(semester: Int, schoolYear: Int) {
subtitleString = getString(R.string.grade_subtitle, semester, schoolYear, schoolYear + 1)
(activity as MainView).setViewSubTitle(subtitleString)
}
fun onChildRefresh() {
presenter.onChildViewRefresh()
}

View File

@ -11,11 +11,10 @@ import io.github.wulkanowy.ui.modules.grade.statistics.GradeStatisticsFragment
import io.github.wulkanowy.ui.modules.grade.summary.GradeSummaryFragment
@Suppress("unused")
@Module(includes = [GradeModule.Static::class])
@Module
abstract class GradeModule {
@Module
object Static {
companion object {
@PerFragment
@Provides

View File

@ -7,7 +7,9 @@ import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.getCurrentOrLast
import timber.log.Timber
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class GradePresenter @Inject constructor(
@ -21,6 +23,8 @@ class GradePresenter @Inject constructor(
var selectedIndex = 0
private set
private var schoolYear = 0
private var semesters = emptyList<Semester>()
private val loadedSemesterId = mutableMapOf<Int, Int>()
@ -56,6 +60,7 @@ class GradePresenter @Inject constructor(
selectedIndex = index + 1
loadedSemesterId.clear()
view?.let {
it.setCurrentSemesterName(index + 1, schoolYear)
notifyChildrenSemesterChange()
loadChild(it.currentPageIndex)
}
@ -95,17 +100,17 @@ class GradePresenter @Inject constructor(
private fun loadData() {
Timber.i("Loading grade data started")
disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getSemesters(it) }
.doOnSuccess {
it.first { item -> item.isCurrent }.also { current ->
selectedIndex = if (selectedIndex == 0) current.semesterName else selectedIndex
semesters = it.filter { semester -> semester.diaryId == current.diaryId }
}
}
.flatMap { semesterRepository.getSemesters(it, refreshOnNoCurrent = true) }
.delay(200, MILLISECONDS)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally { view?.showProgress(false) }
.subscribe({
val current = it.getCurrentOrLast()
selectedIndex = if (selectedIndex == 0) current.semesterName else selectedIndex
schoolYear = current.schoolYear
semesters = it.filter { semester -> semester.diaryId == current.diaryId }
view?.setCurrentSemesterName(current.semesterName, schoolYear)
view?.run {
Timber.i("Loading grade result: Attempt load index $currentPageIndex")
loadChild(currentPageIndex)
@ -121,6 +126,7 @@ class GradePresenter @Inject constructor(
private fun showErrorViewOnError(message: String, error: Throwable) {
lastError = error
view?.run {
showProgress(false)
showErrorView(true)
setErrorDetails(message)
}

View File

@ -20,6 +20,8 @@ interface GradeView : BaseView {
fun showSemesterDialog(selectedIndex: Int)
fun setCurrentSemesterName(semester: Int, schoolYear: Int)
fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean)
fun notifyChildParentReselected(index: Int)

View File

@ -0,0 +1,209 @@
package io.github.wulkanowy.ui.modules.grade.statistics
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.github.mikephil.charting.components.Legend
import com.github.mikephil.charting.components.LegendEntry
import com.github.mikephil.charting.data.BarData
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.PieData
import com.github.mikephil.charting.data.PieDataSet
import com.github.mikephil.charting.data.PieEntry
import com.github.mikephil.charting.formatter.ValueFormatter
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.utils.getThemeAttrColor
import kotlinx.android.synthetic.main.item_grade_statistics_bar.view.*
import kotlinx.android.synthetic.main.item_grade_statistics_pie.view.*
import javax.inject.Inject
class GradeStatisticsAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var items = emptyList<GradeStatisticsItem>()
var theme: String = "vulcan"
private val vulcanGradeColors = listOf(
6 to R.color.grade_vulcan_six,
5 to R.color.grade_vulcan_five,
4 to R.color.grade_vulcan_four,
3 to R.color.grade_vulcan_three,
2 to R.color.grade_vulcan_two,
1 to R.color.grade_vulcan_one
)
private val materialGradeColors = listOf(
6 to R.color.grade_material_six,
5 to R.color.grade_material_five,
4 to R.color.grade_material_four,
3 to R.color.grade_material_three,
2 to R.color.grade_material_two,
1 to R.color.grade_material_one
)
private val gradePointsColors = listOf(
Color.parseColor("#37c69c"),
Color.parseColor("#d8b12a")
)
private val gradeLabels = listOf(
"6, 6-", "5, 5-, 5+", "4, 4-, 4+", "3, 3-, 3+", "2, 2-, 2+", "1, 1+"
)
override fun getItemCount() = items.size
override fun getItemViewType(position: Int): Int {
return when (items[position].type) {
ViewType.SEMESTER, ViewType.PARTIAL -> R.layout.item_grade_statistics_pie
ViewType.POINTS -> R.layout.item_grade_statistics_bar
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val viewHolder = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
R.layout.item_grade_statistics_bar -> GradeStatisticsBar(viewHolder)
else -> GradeStatisticsPie(viewHolder)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is GradeStatisticsPie -> bindPieChart(holder, items[position].partial)
is GradeStatisticsBar -> bindBarChart(holder, items[position].points!!)
}
}
private fun bindPieChart(holder: GradeStatisticsPie, partials: List<GradeStatistics>) {
with(holder.view.gradeStatisticsPieTitle) {
text = partials.firstOrNull()?.subject
visibility = if (items.size == 1) GONE else VISIBLE
}
val gradeColors = when (theme) {
"vulcan" -> vulcanGradeColors
else -> materialGradeColors
}
val dataset = PieDataSet(partials.map {
PieEntry(it.amount.toFloat(), it.grade.toString())
}, "Legenda")
with(dataset) {
valueTextSize = 12f
sliceSpace = 1f
valueTextColor = Color.WHITE
setColors(partials.map {
gradeColors.single { color -> color.first == it.grade }.second
}.toIntArray(), holder.view.context)
}
with(holder.view.gradeStatisticsPie) {
setTouchEnabled(false)
if (partials.size == 1) animateXY(1000, 1000)
data = PieData(dataset).apply {
setValueFormatter(object : ValueFormatter() {
override fun getPieLabel(value: Float, pieEntry: PieEntry): String {
return resources.getQuantityString(R.plurals.grade_number_item, value.toInt(), value.toInt())
}
})
}
with(legend) {
textColor = context.getThemeAttrColor(android.R.attr.textColorPrimary)
setCustom(gradeLabels.mapIndexed { i, it ->
LegendEntry().apply {
label = it
formColor = ContextCompat.getColor(context, gradeColors[i].second)
form = Legend.LegendForm.SQUARE
}
})
}
minAngleForSlices = 25f
description.isEnabled = false
centerText = partials.fold(0) { acc, it -> acc + it.amount }
.let { resources.getQuantityString(R.plurals.grade_number_item, it, it) }
setHoleColor(context.getThemeAttrColor(android.R.attr.windowBackground))
setCenterTextColor(context.getThemeAttrColor(android.R.attr.textColorPrimary))
invalidate()
}
}
private fun bindBarChart(holder: GradeStatisticsBar, points: GradePointsStatistics) {
with(holder.view.gradeStatisticsBarTitle) {
text = points.subject
visibility = if (items.size == 1) GONE else VISIBLE
}
val dataset = BarDataSet(listOf(
BarEntry(1f, points.others.toFloat()),
BarEntry(2f, points.student.toFloat())
), "Legenda")
with(dataset) {
valueTextSize = 12f
valueTextColor = holder.view.context.getThemeAttrColor(android.R.attr.textColorPrimary)
valueFormatter = object : ValueFormatter() {
override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}%"
}
colors = gradePointsColors
}
with(holder.view.gradeStatisticsBar) {
setTouchEnabled(false)
if (items.size == 1) animateXY(1000, 1000)
data = BarData(dataset).apply {
barWidth = 0.5f
setFitBars(true)
}
legend.setCustom(listOf(
LegendEntry().apply {
label = "Średnia klasy"
formColor = gradePointsColors[0]
form = Legend.LegendForm.SQUARE
},
LegendEntry().apply {
label = "Uczeń"
formColor = gradePointsColors[1]
form = Legend.LegendForm.SQUARE
}
))
legend.textColor = context.getThemeAttrColor(android.R.attr.textColorPrimary)
description.isEnabled = false
holder.view.context.getThemeAttrColor(android.R.attr.textColorPrimary).let {
axisLeft.textColor = it
axisRight.textColor = it
}
xAxis.setDrawLabels(false)
xAxis.setDrawGridLines(false)
with(axisLeft) {
axisMinimum = 0f
axisMaximum = 100f
labelCount = 11
}
with(axisRight) {
axisMinimum = 0f
axisMaximum = 100f
labelCount = 11
}
invalidate()
}
}
class GradeStatisticsPie(val view: View) : RecyclerView.ViewHolder(view)
class GradeStatisticsBar(val view: View) : RecyclerView.ViewHolder(view)
}

View File

@ -1,31 +1,18 @@
package io.github.wulkanowy.ui.modules.grade.statistics
import android.graphics.Color
import android.graphics.Color.WHITE
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.github.mikephil.charting.components.Legend
import com.github.mikephil.charting.components.LegendEntry
import com.github.mikephil.charting.data.BarData
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.PieData
import com.github.mikephil.charting.data.PieDataSet
import com.github.mikephil.charting.data.PieEntry
import com.github.mikephil.charting.formatter.ValueFormatter
import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.setOnItemSelectedListener
import kotlinx.android.synthetic.main.fragment_grade_statistics.*
import javax.inject.Inject
@ -35,6 +22,9 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
@Inject
lateinit var presenter: GradeStatisticsPresenter
@Inject
lateinit var statisticsAdapter: GradeStatisticsAdapter
private lateinit var subjectsAdapter: ArrayAdapter<String>
companion object {
@ -43,9 +33,7 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
fun newInstance() = GradeStatisticsFragment()
}
override val isPieViewEmpty get() = gradeStatisticsChart.isEmpty
override val isBarViewEmpty get() = gradeStatisticsChartPoints.isEmpty
override val isViewEmpty get() = statisticsAdapter.items.isEmpty()
override val currentType
get() = when (gradeStatisticsTypeSwitch.checkedRadioButtonId) {
@ -54,35 +42,6 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
else -> ViewType.POINTS
}
private lateinit var gradeColors: List<Pair<Int, Int>>
private val vulcanGradeColors = listOf(
6 to R.color.grade_vulcan_six,
5 to R.color.grade_vulcan_five,
4 to R.color.grade_vulcan_four,
3 to R.color.grade_vulcan_three,
2 to R.color.grade_vulcan_two,
1 to R.color.grade_vulcan_one
)
private val materialGradeColors = listOf(
6 to R.color.grade_material_six,
5 to R.color.grade_material_five,
4 to R.color.grade_material_four,
3 to R.color.grade_material_three,
2 to R.color.grade_material_two,
1 to R.color.grade_material_one
)
private val gradePointsColors = listOf(
Color.parseColor("#37c69c"),
Color.parseColor("#d8b12a")
)
private val gradeLabels = listOf(
"6, 6-", "5, 5-, 5+", "4, 4-, 4+", "3, 3-, 3+", "2, 2-, 2+", "1, 1+"
)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_grade_statistics, container, false)
}
@ -94,31 +53,9 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
}
override fun initView() {
with(gradeStatisticsChart) {
description.isEnabled = false
setHoleColor(context.getThemeAttrColor(android.R.attr.windowBackground))
setCenterTextColor(context.getThemeAttrColor(android.R.attr.textColorPrimary))
animateXY(1000, 1000)
minAngleForSlices = 25f
legend.textColor = context.getThemeAttrColor(android.R.attr.textColorPrimary)
}
with(gradeStatisticsChartPoints) {
description.isEnabled = false
animateXY(1000, 1000)
legend.textColor = context.getThemeAttrColor(android.R.attr.textColorPrimary)
with(axisLeft) {
axisMinimum = 0f
axisMaximum = 100f
labelCount = 11
}
with(axisRight) {
axisMinimum = 0f
axisMaximum = 100f
labelCount = 11
}
with(gradeStatisticsRecycler) {
layoutManager = LinearLayoutManager(requireContext())
adapter = statisticsAdapter
}
subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf())
@ -144,86 +81,10 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
}
}
override fun updatePieData(items: List<GradeStatistics>, theme: String) {
gradeColors = when (theme) {
"vulcan" -> vulcanGradeColors
else -> materialGradeColors
}
val dataset = PieDataSet(items.map {
PieEntry(it.amount.toFloat(), it.grade.toString())
}, "Legenda").apply {
valueTextSize = 12f
sliceSpace = 1f
valueTextColor = WHITE
setColors(items.map {
gradeColors.single { color -> color.first == it.grade }.second
}.toIntArray(), context)
}
with(gradeStatisticsChart) {
data = PieData(dataset).apply {
setTouchEnabled(false)
setValueFormatter(object : ValueFormatter() {
override fun getPieLabel(value: Float, pieEntry: PieEntry): String {
return resources.getQuantityString(R.plurals.grade_number_item, value.toInt(), value.toInt())
}
})
centerText = items.fold(0) { acc, it -> acc + it.amount }
.let { resources.getQuantityString(R.plurals.grade_number_item, it, it) }
}
legend.apply {
setCustom(gradeLabels.mapIndexed { i, it ->
LegendEntry().apply {
label = it
formColor = ContextCompat.getColor(context, gradeColors[i].second)
form = Legend.LegendForm.SQUARE
}
})
}
invalidate()
}
}
override fun updateBarData(item: GradePointsStatistics) {
val dataset = BarDataSet(listOf(
BarEntry(1f, item.others.toFloat()),
BarEntry(2f, item.student.toFloat())
), "Legenda").apply {
valueTextSize = 12f
valueTextColor = requireContext().getThemeAttrColor(android.R.attr.textColorPrimary)
valueFormatter = object : ValueFormatter() {
override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}%"
}
colors = gradePointsColors
}
with(gradeStatisticsChartPoints) {
data = BarData(dataset).apply {
barWidth = 0.5f
setFitBars(true)
}
setTouchEnabled(false)
xAxis.setDrawLabels(false)
xAxis.setDrawGridLines(false)
requireContext().getThemeAttrColor(android.R.attr.textColorPrimary).let {
axisLeft.textColor = it
axisRight.textColor = it
}
legend.setCustom(listOf(
LegendEntry().apply {
label = "Średnia klasy"
formColor = gradePointsColors[0]
form = Legend.LegendForm.SQUARE
},
LegendEntry().apply {
label = "Uczeń"
formColor = gradePointsColors[1]
form = Legend.LegendForm.SQUARE
}
))
invalidate()
}
override fun updateData(items: List<GradeStatisticsItem>, theme: String) {
statisticsAdapter.theme = theme
statisticsAdapter.items = items
statisticsAdapter.notifyDataSetChanged()
}
override fun showSubjects(show: Boolean) {
@ -232,16 +93,15 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
}
override fun clearView() {
gradeStatisticsChart.clear()
gradeStatisticsChartPoints.clear()
statisticsAdapter.items = emptyList()
}
override fun showPieContent(show: Boolean) {
gradeStatisticsChart.visibility = if (show) View.VISIBLE else View.GONE
override fun resetView() {
gradeStatisticsScroll.scrollTo(0, 0)
}
override fun showBarContent(show: Boolean) {
gradeStatisticsChartPoints.visibility = if (show) View.VISIBLE else View.GONE
override fun showContent(show: Boolean) {
gradeStatisticsRecycler.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showEmpty(show: Boolean) {
@ -273,7 +133,7 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
}
override fun onParentReselected() {
//
presenter.onParentViewReselected()
}
override fun onParentChangeSemester() {

View File

@ -48,12 +48,19 @@ class GradeStatisticsPresenter @Inject constructor(
loadDataByType(semesterId, currentSubjectName, currentType, forceRefresh)
}
fun onParentViewReselected() {
view?.run {
if (!isViewEmpty) resetView()
}
}
fun onParentViewChangeSemester() {
view?.run {
showProgress(true)
enableSwipe(false)
showRefresh(false)
showBarContent(false)
showContent(false)
showErrorView(false)
showEmpty(false)
clearView()
@ -81,8 +88,7 @@ class GradeStatisticsPresenter @Inject constructor(
fun onSubjectSelected(name: String?) {
Timber.i("Select grade stats subject $name")
view?.run {
showBarContent(false)
showPieContent(false)
showContent(false)
showProgress(true)
enableSwipe(false)
showEmpty(false)
@ -99,8 +105,7 @@ class GradeStatisticsPresenter @Inject constructor(
Timber.i("Select grade stats semester: $type")
disposable.clear()
view?.run {
showBarContent(false)
showPieContent(false)
showContent(false)
showProgress(true)
enableSwipe(false)
showEmpty(false)
@ -135,20 +140,22 @@ class GradeStatisticsPresenter @Inject constructor(
private fun loadDataByType(semesterId: Int, subjectName: String, type: ViewType, forceRefresh: Boolean = false) {
currentSubjectName = subjectName
currentType = type
when (type) {
ViewType.SEMESTER -> loadData(semesterId, subjectName, true, forceRefresh)
ViewType.PARTIAL -> loadData(semesterId, subjectName, false, forceRefresh)
ViewType.POINTS -> loadPointsData(semesterId, subjectName, forceRefresh)
}
loadData(semesterId, subjectName, type, forceRefresh)
}
private fun loadData(semesterId: Int, subjectName: String, isSemester: Boolean, forceRefresh: Boolean = false) {
private fun loadData(semesterId: Int, subjectName: String, type: ViewType, forceRefresh: Boolean) {
Timber.i("Loading grade stats data started")
disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getSemesters(it) }
.flatMap { gradeStatisticsRepository.getGradesStatistics(it.first { item -> item.semesterId == semesterId }, subjectName, isSemester, forceRefresh) }
.map { list -> list.sortedByDescending { it.grade } }
.map { list -> list.filter { it.amount != 0 } }
.flatMap {
val semester = it.first { item -> item.semesterId == semesterId }
when (type) {
ViewType.SEMESTER -> gradeStatisticsRepository.getGradesStatistics(semester, subjectName, true, forceRefresh)
ViewType.PARTIAL -> gradeStatisticsRepository.getGradesStatistics(semester, subjectName, false, forceRefresh)
ViewType.POINTS -> gradeStatisticsRepository.getGradesPointsStatistics(semester, subjectName, forceRefresh)
}
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
@ -163,10 +170,9 @@ class GradeStatisticsPresenter @Inject constructor(
Timber.i("Loading grade stats result: Success")
view?.run {
showEmpty(it.isEmpty())
showBarContent(false)
showPieContent(it.isNotEmpty())
showContent(it.isNotEmpty())
showErrorView(false)
updatePieData(it, preferencesRepository.gradeColorTheme)
updateData(it, preferencesRepository.gradeColorTheme)
}
analytics.logEvent("load_grade_statistics", "items" to it.size, "force_refresh" to forceRefresh)
}) {
@ -175,47 +181,9 @@ class GradeStatisticsPresenter @Inject constructor(
})
}
private fun loadPointsData(semesterId: Int, subjectName: String, forceRefresh: Boolean = false) {
Timber.i("Loading grade points stats data started")
disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getSemesters(it) }
.flatMapMaybe { gradeStatisticsRepository.getGradesPointsStatistics(it.first { item -> item.semesterId == semesterId }, subjectName, forceRefresh) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
view?.run {
showRefresh(false)
showProgress(false)
enableSwipe(true)
notifyParentDataLoaded(semesterId)
}
}
.subscribe({
Timber.i("Loading grade points stats result: Success")
view?.run {
showEmpty(false)
showPieContent(false)
showBarContent(true)
showErrorView(false)
updateBarData(it)
}
analytics.logEvent("load_grade_points_statistics", "force_refresh" to forceRefresh)
}, {
Timber.e("Loading grade points stats result: An exception occurred")
errorHandler.dispatch(it)
}, {
Timber.d("Loading grade points stats result: No point stats found")
view?.run {
showBarContent(false)
showEmpty(true)
}
})
)
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if ((isBarViewEmpty && currentType == ViewType.POINTS) || (isPieViewEmpty) && currentType != ViewType.POINTS) {
if (isViewEmpty) {
lastError = error
setErrorDetails(message)
showErrorView(true)

View File

@ -1,14 +1,11 @@
package io.github.wulkanowy.ui.modules.grade.statistics
import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.ui.base.BaseView
interface GradeStatisticsView : BaseView {
val isPieViewEmpty: Boolean
val isBarViewEmpty: Boolean
val isViewEmpty: Boolean
val currentType: ViewType
@ -16,9 +13,7 @@ interface GradeStatisticsView : BaseView {
fun updateSubjects(data: ArrayList<String>)
fun updatePieData(items: List<GradeStatistics>, theme: String)
fun updateBarData(item: GradePointsStatistics)
fun updateData(items: List<GradeStatisticsItem>, theme: String)
fun showSubjects(show: Boolean)
@ -28,9 +23,9 @@ interface GradeStatisticsView : BaseView {
fun clearView()
fun showPieContent(show: Boolean)
fun resetView()
fun showBarContent(show: Boolean)
fun showContent(show: Boolean)
fun showEmpty(show: Boolean)

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment
import io.github.wulkanowy.ui.modules.login.form.LoginFormFragment
import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment
import io.github.wulkanowy.ui.modules.login.studentselect.LoginStudentSelectFragment
import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment
import io.github.wulkanowy.utils.setOnSelectPageListener
@ -52,7 +53,8 @@ class LoginActivity : BaseActivity<LoginPresenter>(), LoginView {
LoginFormFragment.newInstance(),
LoginSymbolFragment.newInstance(),
LoginStudentSelectFragment.newInstance(),
LoginAdvancedFragment.newInstance()
LoginAdvancedFragment.newInstance(),
LoginRecoverFragment.newInstance()
))
}
@ -99,4 +101,8 @@ class LoginActivity : BaseActivity<LoginPresenter>(), LoginView {
fun onAdvancedLoginClick() {
presenter.onAdvancedLoginClick()
}
fun onRecoverClick() {
presenter.onRecoverClick()
}
}

View File

@ -43,5 +43,8 @@ class LoginErrorHandler @Inject constructor(
super.clear()
onBadCredentials = {}
onStudentDuplicate = {}
onInvalidToken = {}
onInvalidPin = {}
onInvalidSymbol = {}
}
}

View File

@ -8,15 +8,15 @@ import io.github.wulkanowy.di.scopes.PerFragment
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment
import io.github.wulkanowy.ui.modules.login.form.LoginFormFragment
import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment
import io.github.wulkanowy.ui.modules.login.studentselect.LoginStudentSelectFragment
import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment
@Suppress("unused")
@Module(includes = [LoginModule.Static::class])
@Module
internal abstract class LoginModule {
@Module
object Static {
companion object {
@PerActivity
@Provides
@ -38,4 +38,8 @@ internal abstract class LoginModule {
@PerFragment
@ContributesAndroidInjector
abstract fun bindLoginSelectStudentFragment(): LoginStudentSelectFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindLoginRecoverFragment(): LoginRecoverFragment
}

View File

@ -49,11 +49,15 @@ class LoginPresenter @Inject constructor(
view?.switchView(3)
}
fun onRecoverClick() {
view?.switchView(4)
}
fun onViewSelected(index: Int) {
view?.apply {
when (index) {
0 -> showActionBar(false)
1, 2 -> showActionBar(true)
1, 2, 3, 4 -> showActionBar(true)
}
}
}
@ -62,7 +66,7 @@ class LoginPresenter @Inject constructor(
Timber.i("Back pressed in login view")
view?.apply {
when (currentViewIndex) {
1, 2, 3 -> switchView(0)
1, 2, 3, 4 -> switchView(0)
else -> default()
}
}

View File

@ -3,10 +3,10 @@ package io.github.wulkanowy.ui.modules.login.advanced
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.ArrayAdapter
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.widget.doOnTextChanged
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
@ -15,6 +15,7 @@ import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.form.LoginSymbolAdapter
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.setOnEditorDoneSignIn
import io.github.wulkanowy.utils.showSoftInput
import kotlinx.android.synthetic.main.fragment_login_advanced.*
import javax.inject.Inject
@ -35,8 +36,8 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView {
else -> "HYBRID"
}
override val formNameValue: String
get() = loginFormName.text.toString().trim()
override val formUsernameValue: String
get() = loginFormUsername.text.toString().trim()
override val formPassValue: String
get() = loginFormPass.text.toString().trim()
@ -45,8 +46,13 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView {
private lateinit var hostValues: Array<String>
override val formHostValue: String?
get() = hostValues.getOrNull(hostKeys.indexOf(loginFormHost.text.toString()))
private lateinit var hostSymbols: Array<String>
override val formHostValue: String
get() = hostValues.getOrNull(hostKeys.indexOf(loginFormHost.text.toString())).orEmpty()
override val formHostSymbol: String
get() = hostSymbols.getOrNull(hostKeys.indexOf(loginFormHost.text.toString())).orEmpty()
override val formPinValue: String
get() = loginFormPin.text.toString().trim()
@ -57,6 +63,12 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView {
override val formTokenValue: String
get() = loginFormToken.text.toString().trim()
override val nicknameLabel: String
get() = getString(R.string.login_nickname_hint)
override val emailLabel: String
get() = getString(R.string.login_email_hint)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_login_advanced, container, false)
}
@ -69,8 +81,9 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView {
override fun initView() {
hostKeys = resources.getStringArray(R.array.hosts_keys)
hostValues = resources.getStringArray(R.array.hosts_values)
hostSymbols = resources.getStringArray(R.array.hosts_symbols)
loginFormName.doOnTextChanged { _, _, _, _ -> presenter.onNameTextChanged() }
loginFormUsername.doOnTextChanged { _, _, _, _ -> presenter.onUsernameTextChanged() }
loginFormPass.doOnTextChanged { _, _, _, _ -> presenter.onPassTextChanged() }
loginFormPin.doOnTextChanged { _, _, _, _ -> presenter.onPinTextChanged() }
loginFormSymbol.doOnTextChanged { _, _, _, _ -> presenter.onSymbolTextChanged() }
@ -86,33 +99,48 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView {
})
}
loginFormPin.setOnEditorDoneSignIn()
loginFormPass.setOnEditorDoneSignIn()
loginFormPin.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() }
loginFormPass.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() }
loginFormSymbol.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values)))
with(loginFormHost) {
setText(hostKeys.getOrElse(0) { "" })
setText(hostKeys.getOrNull(0).orEmpty())
setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys))
setOnClickListener { if (loginFormContainer.visibility == GONE) dismissDropDown() }
}
}
private fun AppCompatEditText.setOnEditorDoneSignIn() {
setOnEditorActionListener { _, id, _ ->
if (id == EditorInfo.IME_ACTION_DONE || id == EditorInfo.IME_NULL) loginFormSignIn.callOnClick() else false
}
override fun showMobileApiWarningMessage() {
loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_mobile_api)
}
override fun setDefaultCredentials(name: String, pass: String, symbol: String, token: String, pin: String) {
loginFormName.setText(name)
override fun showScraperWarningMessage() {
loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_scraper)
}
override fun showHybridWarningMessage() {
loginFormAdvancedWarningInfo.text = getString(R.string.login_advanced_warning_hybrid)
}
override fun setDefaultCredentials(username: String, pass: String, symbol: String, token: String, pin: String) {
loginFormUsername.setText(username)
loginFormPass.setText(pass)
loginFormToken.setText(token)
loginFormSymbol.setText(symbol)
loginFormPin.setText(pin)
}
override fun setErrorNameRequired() {
with(loginFormNameLayout) {
override fun setUsernameLabel(label: String) {
loginFormUsernameLayout.hint = label
}
override fun setSymbol(symbol: String) {
loginFormSymbol.setText(symbol)
}
override fun setErrorUsernameRequired() {
with(loginFormUsernameLayout) {
requestFocus()
error = getString(R.string.login_field_required)
}
@ -181,8 +209,8 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView {
}
}
override fun clearNameError() {
loginFormNameLayout.error = null
override fun clearUsernameError() {
loginFormUsernameLayout.error = null
}
override fun clearPassError() {
@ -202,30 +230,30 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView {
}
override fun showOnlyHybridModeInputs() {
loginFormNameLayout.visibility = View.VISIBLE
loginFormPassLayout.visibility = View.VISIBLE
loginFormHostLayout.visibility = View.VISIBLE
loginFormPinLayout.visibility = View.GONE
loginFormSymbolLayout.visibility = View.VISIBLE
loginFormTokenLayout.visibility = View.GONE
loginFormUsernameLayout.visibility = VISIBLE
loginFormPassLayout.visibility = VISIBLE
loginFormHostLayout.visibility = VISIBLE
loginFormPinLayout.visibility = GONE
loginFormSymbolLayout.visibility = VISIBLE
loginFormTokenLayout.visibility = GONE
}
override fun showOnlyScrapperModeInputs() {
loginFormNameLayout.visibility = View.VISIBLE
loginFormPassLayout.visibility = View.VISIBLE
loginFormHostLayout.visibility = View.VISIBLE
loginFormPinLayout.visibility = View.GONE
loginFormSymbolLayout.visibility = View.VISIBLE
loginFormTokenLayout.visibility = View.GONE
loginFormUsernameLayout.visibility = VISIBLE
loginFormPassLayout.visibility = VISIBLE
loginFormHostLayout.visibility = VISIBLE
loginFormPinLayout.visibility = GONE
loginFormSymbolLayout.visibility = VISIBLE
loginFormTokenLayout.visibility = GONE
}
override fun showOnlyMobileApiModeInputs() {
loginFormNameLayout.visibility = View.GONE
loginFormPassLayout.visibility = View.GONE
loginFormHostLayout.visibility = View.GONE
loginFormPinLayout.visibility = View.VISIBLE
loginFormSymbolLayout.visibility = View.VISIBLE
loginFormTokenLayout.visibility = View.VISIBLE
loginFormUsernameLayout.visibility = GONE
loginFormPassLayout.visibility = GONE
loginFormHostLayout.visibility = GONE
loginFormPinLayout.visibility = VISIBLE
loginFormSymbolLayout.visibility = VISIBLE
loginFormTokenLayout.visibility = VISIBLE
}
override fun showSoftKeyboard() {
@ -237,21 +265,26 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView {
}
override fun showProgress(show: Boolean) {
loginFormProgress.visibility = if (show) View.VISIBLE else View.GONE
loginFormProgress.visibility = if (show) VISIBLE else GONE
}
override fun showContent(show: Boolean) {
loginFormContainer.visibility = if (show) View.VISIBLE else View.GONE
loginFormContainer.visibility = if (show) VISIBLE else GONE
}
override fun notifyParentAccountLogged(students: List<Student>) {
(activity as? LoginActivity)?.onFormFragmentAccountLogged(students, Triple(
loginFormName.text.toString(),
loginFormUsername.text.toString(),
loginFormPass.text.toString(),
resources.getStringArray(R.array.hosts_values)[1]
))
}
override fun onResume() {
super.onResume()
presenter.updateUsernameLabel()
}
override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView()

View File

@ -65,22 +65,39 @@ class LoginAdvancedPresenter @Inject constructor(
}
}
fun updateUsernameLabel() {
view?.apply {
setUsernameLabel(if ("vulcan" in formHostValue || "fakelog" in formHostValue) emailLabel else nicknameLabel)
}
}
fun onHostSelected() {
view?.apply {
clearPassError()
clearNameError()
if (formHostValue?.contains("fakelog") == true) {
clearUsernameError()
if (formHostValue.contains("fakelog")) {
setDefaultCredentials("jan@fakelog.cf", "jan123", "powiatwulkanowy", "FK100000", "999999")
}
setSymbol(formHostSymbol)
updateUsernameLabel()
}
}
fun onLoginModeSelected(type: Sdk.Mode) {
view?.run {
when (type) {
Sdk.Mode.API -> showOnlyMobileApiModeInputs()
Sdk.Mode.SCRAPPER -> showOnlyScrapperModeInputs()
Sdk.Mode.HYBRID -> showOnlyHybridModeInputs()
Sdk.Mode.API -> {
showOnlyMobileApiModeInputs()
showMobileApiWarningMessage()
}
Sdk.Mode.SCRAPPER -> {
showOnlyScrapperModeInputs()
showScraperWarningMessage()
}
Sdk.Mode.HYBRID -> {
showOnlyHybridModeInputs()
showHybridWarningMessage()
}
}
}
}
@ -89,8 +106,8 @@ class LoginAdvancedPresenter @Inject constructor(
view?.clearPassError()
}
fun onNameTextChanged() {
view?.clearNameError()
fun onUsernameTextChanged() {
view?.clearUsernameError()
}
fun onPinTextChanged() {
@ -137,7 +154,7 @@ class LoginAdvancedPresenter @Inject constructor(
}
private fun getStudentsAppropriatesToLoginType(): Single<List<Student>> {
val email = view?.formNameValue.orEmpty()
val email = view?.formUsernameValue.orEmpty()
val password = view?.formPassValue.orEmpty()
val endpoint = view?.formHostValue.orEmpty()
@ -153,7 +170,7 @@ class LoginAdvancedPresenter @Inject constructor(
}
private fun validateCredentials(): Boolean {
val login = view?.formNameValue.orEmpty()
val login = view?.formUsernameValue.orEmpty()
val password = view?.formPassValue.orEmpty()
val pin = view?.formPinValue.orEmpty()
@ -181,7 +198,7 @@ class LoginAdvancedPresenter @Inject constructor(
}
Sdk.Mode.SCRAPPER -> {
if (login.isEmpty()) {
view?.setErrorNameRequired()
view?.setErrorUsernameRequired()
isCorrect = false
}
@ -197,7 +214,7 @@ class LoginAdvancedPresenter @Inject constructor(
}
Sdk.Mode.HYBRID -> {
if (login.isEmpty()) {
view?.setErrorNameRequired()
view?.setErrorUsernameRequired()
isCorrect = false
}

View File

@ -5,11 +5,13 @@ import io.github.wulkanowy.ui.base.BaseView
interface LoginAdvancedView : BaseView {
val formNameValue: String
val formUsernameValue: String
val formPassValue: String
val formHostValue: String?
val formHostValue: String
val formHostSymbol: String
val formLoginType: String
@ -19,11 +21,25 @@ interface LoginAdvancedView : BaseView {
val formTokenValue: String
val nicknameLabel: String
val emailLabel: String
fun initView()
fun setDefaultCredentials(name: String, pass: String, symbol: String, token: String, pin: String)
fun showMobileApiWarningMessage()
fun setErrorNameRequired()
fun showScraperWarningMessage()
fun showHybridWarningMessage()
fun setDefaultCredentials(username: String, pass: String, symbol: String, token: String, pin: String)
fun setUsernameLabel(label: String)
fun setSymbol(symbol: String)
fun setErrorUsernameRequired()
fun setErrorPassRequired(focus: Boolean)
@ -31,7 +47,7 @@ interface LoginAdvancedView : BaseView {
fun setErrorPassIncorrect()
fun clearNameError()
fun clearUsernameError()
fun clearPassError()

View File

@ -7,8 +7,7 @@ import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
import android.view.inputmethod.EditorInfo.IME_NULL
import android.widget.ArrayAdapter
import androidx.core.widget.doOnTextChanged
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
@ -18,6 +17,7 @@ import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.setOnEditorDoneSignIn
import io.github.wulkanowy.utils.showSoftInput
import kotlinx.android.synthetic.main.fragment_login_form.*
import javax.inject.Inject
@ -34,16 +34,33 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
fun newInstance() = LoginFormFragment()
}
override val formNameValue get() = loginFormName.text.toString()
override val formUsernameValue: String
get() = loginFormUsername.text.toString()
override val formPassValue get() = loginFormPass.text.toString()
override val formPassValue: String
get() = loginFormPass.text.toString()
override val formHostValue get() = hostValues.getOrNull(hostKeys.indexOf(loginFormHost.text.toString()))
override val formHostValue: String
get() = hostValues.getOrNull(hostKeys.indexOf(loginFormHost.text.toString())).orEmpty()
override val formHostSymbol: String
get() = hostSymbols.getOrNull(hostKeys.indexOf(loginFormHost.text.toString())).orEmpty()
override val formSymbolValue: String
get() = loginFormSymbol.text.toString()
override val nicknameLabel: String
get() = getString(R.string.login_nickname_hint)
override val emailLabel: String
get() = getString(R.string.login_email_hint)
private lateinit var hostKeys: Array<String>
private lateinit var hostValues: Array<String>
private lateinit var hostSymbols: Array<String>
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_login_form, container, false)
}
@ -56,38 +73,61 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
override fun initView() {
hostKeys = resources.getStringArray(R.array.hosts_keys)
hostValues = resources.getStringArray(R.array.hosts_values)
hostSymbols = resources.getStringArray(R.array.hosts_symbols)
loginFormName.doOnTextChanged { _, _, _, _ -> presenter.onNameTextChanged() }
loginFormUsername.doOnTextChanged { _, _, _, _ -> presenter.onUsernameTextChanged() }
loginFormPass.doOnTextChanged { _, _, _, _ -> presenter.onPassTextChanged() }
loginFormSymbol.doOnTextChanged { _, _, _, _ -> presenter.onSymbolTextChanged() }
loginFormHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() }
loginFormSignIn.setOnClickListener { presenter.onSignInClick() }
loginFormAdvancedButton.setOnClickListener { presenter.onAdvancedLoginClick() }
loginFormPrivacyLink.setOnClickListener { presenter.onPrivacyLinkClick() }
loginFormFaq.setOnClickListener { presenter.onFaqClick() }
loginFormContactEmail.setOnClickListener { presenter.onEmailClick() }
loginFormRecoverLink.setOnClickListener { presenter.onRecoverClick() }
loginFormPass.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() }
loginFormSymbol.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() }
loginFormPass.setOnEditorActionListener { _, id, _ ->
if (id == IME_ACTION_DONE || id == IME_NULL) loginFormSignIn.callOnClick() else false
}
loginFormSymbol.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values)))
with(loginFormHost) {
setText(hostKeys.getOrElse(0) { "" })
setText(hostKeys.getOrNull(0).orEmpty())
setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys))
setOnClickListener { if (loginFormContainer.visibility == GONE) dismissDropDown() }
}
}
override fun setCredentials(name: String, pass: String) {
loginFormName.setText(name)
override fun setCredentials(username: String, pass: String) {
loginFormUsername.setText(username)
loginFormPass.setText(pass)
}
override fun setErrorNameRequired() {
with(loginFormNameLayout) {
override fun setSymbol(symbol: String) {
loginFormSymbol.setText(symbol)
}
override fun setUsernameLabel(label: String) {
loginFormUsernameLayout.hint = label
}
override fun showSymbol(show: Boolean) {
loginFormSymbolLayout.visibility = if (show) VISIBLE else GONE
}
override fun setErrorUsernameRequired() {
with(loginFormUsernameLayout) {
requestFocus()
error = getString(R.string.login_field_required)
}
}
override fun setErrorSymbolRequired(focus: Boolean) {
with(loginFormSymbolLayout) {
if (focus) requestFocus()
error = getString(R.string.login_symbol_helper)
}
}
override fun setErrorPassRequired(focus: Boolean) {
with(loginFormPassLayout) {
if (focus) requestFocus()
@ -109,14 +149,18 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
}
}
override fun clearNameError() {
loginFormNameLayout.error = null
override fun clearUsernameError() {
loginFormUsernameLayout.error = null
}
override fun clearPassError() {
loginFormPassLayout.error = null
}
override fun clearSymbolError() {
loginFormSymbolLayout.error = null
}
override fun showSoftKeyboard() {
activity?.showSoftInput()
}
@ -154,6 +198,10 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
(activity as? LoginActivity)?.onAdvancedLoginClick()
}
override fun onRecoverClick() {
(activity as? LoginActivity)?.onRecoverClick()
}
override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView()
@ -163,6 +211,14 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
context?.openInternetBrowser("https://wulkanowy.github.io/czesto-zadawane-pytania/dlaczego-nie-moge-sie-zalogowac", ::showMessage)
}
override fun onResume() {
super.onResume()
with(presenter) {
updateUsernameLabel()
updateSymbolInputVisibility()
}
}
override fun openEmail() {
context?.openEmailClient(
requireContext().getString(R.string.login_email_intent_title),

View File

@ -42,10 +42,25 @@ class LoginFormPresenter @Inject constructor(
fun onHostSelected() {
view?.apply {
clearPassError()
clearNameError()
if (formHostValue?.contains("fakelog") == true) {
clearUsernameError()
if (formHostValue.contains("fakelog")) {
setCredentials("jan@fakelog.cf", "jan123")
}
setSymbol(formHostSymbol)
updateUsernameLabel()
updateSymbolInputVisibility()
}
}
fun updateUsernameLabel() {
view?.run {
setUsernameLabel(if ("standard" in formHostValue) emailLabel else nicknameLabel)
}
}
fun updateSymbolInputVisibility() {
view?.run {
showSymbol("adfs" in formHostValue)
}
}
@ -53,18 +68,23 @@ class LoginFormPresenter @Inject constructor(
view?.clearPassError()
}
fun onNameTextChanged() {
view?.clearNameError()
fun onUsernameTextChanged() {
view?.clearUsernameError()
}
fun onSymbolTextChanged() {
view?.clearSymbolError()
}
fun onSignInClick() {
val email = view?.formNameValue.orEmpty().trim()
val email = view?.formUsernameValue.orEmpty().trim()
val password = view?.formPassValue.orEmpty().trim()
val endpoint = view?.formHostValue.orEmpty().trim()
val host = view?.formHostValue.orEmpty().trim()
val symbol = view?.formSymbolValue.orEmpty().trim()
if (!validateCredentials(email, password)) return
if (!validateCredentials(email, password, host, symbol)) return
disposable.add(studentRepository.getStudentsScrapper(email, password, endpoint)
disposable.add(studentRepository.getStudentsScrapper(email, password, host, symbol)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doOnSubscribe {
@ -83,11 +103,11 @@ class LoginFormPresenter @Inject constructor(
}
.subscribe({
Timber.i("Login result: Success")
analytics.logEvent("registration_form", "success" to true, "students" to it.size, "scrapperBaseUrl" to endpoint, "error" to "No error")
view?.notifyParentAccountLogged(it, Triple(email, password, endpoint))
analytics.logEvent("registration_form", "success" to true, "students" to it.size, "scrapperBaseUrl" to host, "error" to "No error")
view?.notifyParentAccountLogged(it, Triple(email, password, host))
}, {
Timber.i("Login result: An exception occurred")
analytics.logEvent("registration_form", "success" to false, "students" to -1, "scrapperBaseUrl" to endpoint, "error" to it.message.ifNullOrBlank { "No message" })
analytics.logEvent("registration_form", "success" to false, "students" to -1, "scrapperBaseUrl" to host, "error" to it.message.ifNullOrBlank { "No message" })
loginErrorHandler.dispatch(it)
view?.showContact(true)
}))
@ -101,11 +121,15 @@ class LoginFormPresenter @Inject constructor(
view?.openEmail()
}
private fun validateCredentials(login: String, password: String): Boolean {
fun onRecoverClick() {
view?.onRecoverClick()
}
private fun validateCredentials(login: String, password: String, host: String, symbol: String): Boolean {
var isCorrect = true
if (login.isEmpty()) {
view?.setErrorNameRequired()
view?.setErrorUsernameRequired()
isCorrect = false
}
@ -118,6 +142,12 @@ class LoginFormPresenter @Inject constructor(
view?.setErrorPassInvalid(focus = isCorrect)
isCorrect = false
}
if ("standard" !in host && symbol.isBlank()) {
view?.setErrorSymbolRequired(focus = isCorrect)
isCorrect = false
}
return isCorrect
}
}

View File

@ -7,15 +7,31 @@ interface LoginFormView : BaseView {
fun initView()
val formNameValue: String
val formUsernameValue: String
val formPassValue: String
val formHostValue: String?
val formHostValue: String
fun setCredentials(name: String, pass: String)
val formHostSymbol: String
fun setErrorNameRequired()
val formSymbolValue: String
val nicknameLabel: String
val emailLabel: String
fun setCredentials(username: String, pass: String)
fun setSymbol(symbol: String)
fun setUsernameLabel(label: String)
fun showSymbol(show: Boolean)
fun setErrorUsernameRequired()
fun setErrorSymbolRequired(focus: Boolean)
fun setErrorPassRequired(focus: Boolean)
@ -23,10 +39,12 @@ interface LoginFormView : BaseView {
fun setErrorPassIncorrect()
fun clearNameError()
fun clearUsernameError()
fun clearPassError()
fun clearSymbolError()
fun showSoftKeyboard()
fun hideSoftKeyboard()
@ -48,4 +66,6 @@ interface LoginFormView : BaseView {
fun openEmail()
fun openAdvancedLogin()
fun onRecoverClick()
}

View File

@ -0,0 +1,220 @@
package io.github.wulkanowy.ui.modules.login.recover
import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.ArrayAdapter
import androidx.core.widget.doOnTextChanged
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.form.LoginSymbolAdapter
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.showSoftInput
import kotlinx.android.synthetic.main.fragment_login_recover.*
import javax.inject.Inject
class LoginRecoverFragment : BaseFragment(), LoginRecoverView {
@Inject
lateinit var presenter: LoginRecoverPresenter
companion object {
fun newInstance() = LoginRecoverFragment()
}
private lateinit var hostKeys: Array<String>
private lateinit var hostValues: Array<String>
override val recoverHostValue: String
get() = hostValues.getOrNull(hostKeys.indexOf(loginRecoverHost.text.toString())).orEmpty()
override val recoverNameValue: String
get() = loginRecoverName.text.toString().trim()
override val recoverSymbolValue: String
get() = loginRecoverSymbol.text.toString().trim()
override val emailHintString: String
get() = getString(R.string.login_email_hint)
override val loginPeselEmailHintString: String
get() = getString(R.string.login_login_pesel_email_hint)
override val invalidEmailString: String
get() = getString(R.string.login_invalid_email)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_login_recover, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this)
}
override fun initView() {
loginRecoverWebView.setBackgroundColor(Color.TRANSPARENT)
hostKeys = resources.getStringArray(R.array.hosts_keys)
hostValues = resources.getStringArray(R.array.hosts_values)
loginRecoverName.doOnTextChanged { _, _, _, _ -> presenter.onNameTextChanged() }
loginRecoverSymbol.doOnTextChanged { _, _, _, _ -> presenter.onSymbolTextChanged() }
loginRecoverHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() }
loginRecoverButton.setOnClickListener { presenter.onRecoverClick() }
loginRecoverErrorRetry.setOnClickListener { presenter.onRecoverClick() }
loginRecoverErrorDetails.setOnClickListener { presenter.onDetailsClick() }
loginRecoverLogin.setOnClickListener { (activity as LoginActivity).switchView(0) }
loginRecoverSymbol.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values)))
with(loginRecoverHost) {
setText(hostKeys.getOrNull(0).orEmpty())
setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys))
setOnClickListener { if (loginRecoverFormContainer.visibility == GONE) dismissDropDown() }
}
}
override fun setDefaultCredentials(username: String) {
loginRecoverName.setText(username)
}
override fun setErrorNameRequired() {
with(loginRecoverNameLayout) {
requestFocus()
error = getString(R.string.login_field_required)
}
}
override fun setUsernameHint(hint: String) {
loginRecoverNameLayout.hint = hint
}
override fun setUsernameError(message: String) {
with(loginRecoverNameLayout) {
requestFocus()
error = message
}
}
override fun setSymbolError(focus: Boolean) {
with(loginRecoverSymbolLayout) {
if (focus) requestFocus()
error = getString(R.string.login_field_required)
}
}
override fun clearUsernameError() {
loginRecoverNameLayout.error = null
}
override fun clearSymbolError() {
loginRecoverSymbolLayout.error = null
}
override fun showSymbol(show: Boolean) {
loginRecoverSymbolLayout.visibility = if (show) VISIBLE else GONE
}
override fun showProgress(show: Boolean) {
loginRecoverProgress.visibility = if (show) VISIBLE else GONE
}
override fun showRecoverForm(show: Boolean) {
loginRecoverFormContainer.visibility = if (show) VISIBLE else GONE
}
override fun showCaptcha(show: Boolean) {
loginRecoverCaptchaContainer.visibility = if (show) VISIBLE else GONE
}
override fun showErrorView(show: Boolean) {
loginRecoverError.visibility = if (show) VISIBLE else GONE
}
override fun setErrorDetails(message: String) {
loginRecoverErrorMessage.text = message
}
override fun showSuccessView(show: Boolean) {
loginRecoverSuccess.visibility = if (show) VISIBLE else GONE
}
override fun setSuccessTitle(title: String) {
loginRecoverSuccessTitle.text = title
}
override fun setSuccessMessage(message: String) {
loginRecoverSuccessMessage.text = message
}
override fun showSoftKeyboard() {
activity?.showSoftInput()
}
override fun hideSoftKeyboard() {
activity?.hideSoftInput()
}
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
override fun loadReCaptcha(siteKey: String, url: String) {
val html = """
<div style="position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);" id="recaptcha"></div>
<script src="https://www.google.com/recaptcha/api.js?onload=cl&render=explicit&hl=pl" async defer></script>
<script>var cl=()=>grecaptcha.render("recaptcha",{
sitekey:'$siteKey',
callback:e =>Android.captchaCallback(e)})</script>
""".trimIndent()
with(loginRecoverWebView) {
settings.javaScriptEnabled = true
webViewClient = object : WebViewClient() {
private var recoverWebViewSuccess: Boolean = true
override fun onPageFinished(view: WebView?, url: String?) {
if (recoverWebViewSuccess) {
showCaptcha(true)
showProgress(false)
} else {
showProgress(false)
showErrorView(true)
}
}
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
recoverWebViewSuccess = false
}
}
loadDataWithBaseURL(url, html, "text/html", "UTF-8", null)
addJavascriptInterface(object {
@JavascriptInterface
fun captchaCallback(reCaptchaResponse: String) {
activity?.runOnUiThread {
presenter.onReCaptchaVerified(reCaptchaResponse)
}
}
}, "Android")
}
}
override fun onResume() {
super.onResume()
presenter.updateFields()
}
override fun onDestroyView() {
super.onDestroyView()
loginRecoverWebView.destroy()
presenter.onDetachView()
}
}

View File

@ -0,0 +1,169 @@
package io.github.wulkanowy.ui.modules.login.recover
import io.github.wulkanowy.data.repositories.recover.RecoverRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.ifNullOrBlank
import timber.log.Timber
import javax.inject.Inject
class LoginRecoverPresenter @Inject constructor(
schedulers: SchedulersProvider,
studentRepository: StudentRepository,
private val loginErrorHandler: RecoverErrorHandler,
private val analytics: FirebaseAnalyticsHelper,
private val recoverRepository: RecoverRepository
) : BasePresenter<LoginRecoverView>(loginErrorHandler, studentRepository, schedulers) {
private lateinit var lastError: Throwable
override fun onAttachView(view: LoginRecoverView) {
super.onAttachView(view)
view.initView()
with(loginErrorHandler) {
showErrorMessage = ::showErrorMessage
onInvalidUsername = ::onInvalidUsername
onInvalidCaptcha = ::onInvalidCaptcha
}
}
fun onNameTextChanged() {
view?.clearUsernameError()
}
fun onSymbolTextChanged() {
view?.clearSymbolError()
}
fun onHostSelected() {
view?.run {
if ("fakelog" in recoverHostValue) setDefaultCredentials("jan@fakelog.cf")
clearUsernameError()
updateFields()
}
}
fun updateFields() {
view?.run {
showSymbol("adfs" in recoverHostValue)
setUsernameHint(if ("standard" in recoverHostValue) emailHintString else loginPeselEmailHintString)
}
}
fun onRecoverClick() {
val username = view?.recoverNameValue.orEmpty()
val host = view?.recoverHostValue.orEmpty()
val symbol = view?.recoverSymbolValue.orEmpty()
if (!validateInput(username, host, symbol)) return
disposable.add(recoverRepository.getReCaptchaSiteKey(host, symbol.ifBlank { "Default" })
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doOnSubscribe {
view?.run {
hideSoftKeyboard()
showRecoverForm(false)
showProgress(true)
showErrorView(false)
showCaptcha(false)
}
}
.subscribe({ (resetUrl, siteKey) ->
view?.loadReCaptcha(siteKey, resetUrl)
}) {
Timber.e("Obtain captcha site key result: An exception occurred")
errorHandler.dispatch(it)
})
}
private fun validateInput(username: String, host: String, symbol: String): Boolean {
var isCorrect = true
if (username.isEmpty()) {
view?.setErrorNameRequired()
isCorrect = false
}
if ("standard" in host && "@" !in username) {
view?.setUsernameError(view?.invalidEmailString.orEmpty())
isCorrect = false
}
if ("adfs" in host && symbol.isBlank()) {
view?.setSymbolError(focus = isCorrect)
isCorrect = false
}
return isCorrect
}
fun onReCaptchaVerified(reCaptchaResponse: String) {
val username = view?.recoverNameValue.orEmpty()
val host = view?.recoverHostValue.orEmpty()
val symbol = view?.recoverSymbolValue.ifNullOrBlank { "Default" }
with(disposable) {
clear()
add(recoverRepository.sendRecoverRequest(host, symbol, username, reCaptchaResponse)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doOnSubscribe {
view?.run {
showProgress(true)
showRecoverForm(false)
showCaptcha(false)
}
}
.doFinally {
view?.showProgress(false)
}
.subscribe({
view?.run {
showSuccessView(true)
setSuccessTitle(it.substringBefore(". "))
setSuccessMessage(it.substringAfter(". "))
}
analytics.logEvent("account_recover", "register" to host, "symbol" to symbol, "success" to true)
}) {
Timber.e("Send recover request result: An exception occurred")
errorHandler.dispatch(it)
analytics.logEvent("account_recover", "register" to host, "symbol" to symbol, "success" to false)
})
}
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
private fun showErrorMessage(message: String, error: Throwable) {
view?.run {
lastError = error
showProgress(false)
setErrorDetails(message)
showErrorView(true)
}
}
private fun onInvalidUsername(message: String) {
view?.run {
setUsernameError(message)
showRecoverForm(true)
}
}
private fun onInvalidCaptcha(message: String, error: Throwable) {
view?.run {
lastError = error
setErrorDetails(message)
showCaptcha(false)
showRecoverForm(false)
showErrorView(true)
}
}
}

View File

@ -0,0 +1,58 @@
package io.github.wulkanowy.ui.modules.login.recover
import io.github.wulkanowy.ui.base.BaseView
interface LoginRecoverView : BaseView {
val recoverHostValue: String
val recoverNameValue: String
val recoverSymbolValue: String
val emailHintString: String
val loginPeselEmailHintString: String
val invalidEmailString: String
fun initView()
fun setDefaultCredentials(username: String)
fun clearUsernameError()
fun clearSymbolError()
fun showSymbol(show: Boolean)
fun setErrorNameRequired()
fun setUsernameHint(hint: String)
fun setUsernameError(message: String)
fun setSymbolError(focus: Boolean)
fun showSoftKeyboard()
fun hideSoftKeyboard()
fun showProgress(show: Boolean)
fun showRecoverForm(show: Boolean)
fun showCaptcha(show: Boolean)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
fun showSuccessView(show: Boolean)
fun setSuccessMessage(message: String)
fun setSuccessTitle(title: String)
fun loadReCaptcha(siteKey: String, url: String)
}

View File

@ -0,0 +1,32 @@
package io.github.wulkanowy.ui.modules.login.recover
import android.content.res.Resources
import com.readystatesoftware.chuck.api.ChuckCollector
import io.github.wulkanowy.sdk.scrapper.exception.InvalidCaptchaException
import io.github.wulkanowy.sdk.scrapper.exception.InvalidEmailException
import io.github.wulkanowy.sdk.scrapper.exception.NoAccountFoundException
import io.github.wulkanowy.ui.base.ErrorHandler
import javax.inject.Inject
class RecoverErrorHandler @Inject constructor(
resources: Resources,
chuckCollector: ChuckCollector
) : ErrorHandler(resources, chuckCollector) {
var onInvalidUsername: (String) -> Unit = {}
var onInvalidCaptcha: (String, Throwable) -> Unit = { _, _ -> }
override fun proceed(error: Throwable) {
when (error) {
is InvalidEmailException, is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty())
is InvalidCaptchaException -> onInvalidCaptcha(error.localizedMessage.orEmpty(), error)
else -> super.proceed(error)
}
}
override fun clear() {
super.clear()
onInvalidUsername = {}
}
}

View File

@ -2,6 +2,8 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import android.annotation.SuppressLint
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
@ -11,7 +13,8 @@ import io.github.wulkanowy.data.db.entities.Student
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_login_student_select.*
class LoginStudentSelectItem(val student: Student) : AbstractFlexibleItem<LoginStudentSelectItem.ItemViewHolder>() {
class LoginStudentSelectItem(val student: Student, val alreadySaved: Boolean) :
AbstractFlexibleItem<LoginStudentSelectItem.ItemViewHolder>() {
override fun getLayoutRes() = R.layout.item_login_student_select
@ -24,6 +27,10 @@ class LoginStudentSelectItem(val student: Student) : AbstractFlexibleItem<LoginS
holder.apply {
loginItemName.text = "${student.studentName} ${student.className}"
loginItemSchool.text = student.schoolName
loginItemName.isEnabled = !alreadySaved
loginItemSchool.isEnabled = !alreadySaved
loginItemCheck.isEnabled = !alreadySaved
loginItemSignedIn.visibility = if (alreadySaved) VISIBLE else GONE
}
}
@ -34,6 +41,7 @@ class LoginStudentSelectItem(val student: Student) : AbstractFlexibleItem<LoginS
other as LoginStudentSelectItem
if (student != other.student) return false
if (alreadySaved != other.alreadySaved) return false
return true
}
@ -42,7 +50,8 @@ class LoginStudentSelectItem(val student: Student) : AbstractFlexibleItem<LoginS
return student.hashCode()
}
class ItemViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
class ItemViewHolder(view: View, val adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter), LayoutContainer {
override val containerView: View
get() = itemView
@ -53,7 +62,10 @@ class LoginStudentSelectItem(val student: Student) : AbstractFlexibleItem<LoginS
override fun onClick(view: View?) {
super.onClick(view)
loginItemCheck.apply { isChecked = !isChecked }
if (loginItemCheck.isEnabled) {
loginItemCheck.apply { isChecked = !isChecked }
}
}
}
}

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.ifNullOrBlank
import io.reactivex.Single
import timber.log.Timber
import java.io.Serializable
import javax.inject.Inject
@ -50,7 +51,7 @@ class LoginStudentSelectPresenter @Inject constructor(
}
fun onItemSelected(item: AbstractFlexibleItem<*>?) {
if (item is LoginStudentSelectItem) {
if (item is LoginStudentSelectItem && !item.alreadySaved) {
selectedStudents.removeAll { it == item.student }
.let { if (!it) selectedStudents.add(item.student) }
@ -58,11 +59,33 @@ class LoginStudentSelectPresenter @Inject constructor(
}
}
private fun compareStudents(a: Student, b: Student): Boolean {
return a.email == b.email
&& a.symbol == b.symbol
&& a.studentId == b.studentId
&& a.schoolSymbol == b.schoolSymbol
&& a.classId == b.classId
}
private fun loadData(students: List<Student>) {
this.students = students
view?.apply {
updateData(students.map { LoginStudentSelectItem(it) })
}
disposable.add(studentRepository.getSavedStudents()
.map { savedStudents ->
students.map { student ->
Pair(student, savedStudents.any { compareStudents(student, it) })
}
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
view?.updateData(it.map { studentPair ->
LoginStudentSelectItem(studentPair.first, studentPair.second)
})
}, {
errorHandler.dispatch(it)
view?.updateData(students.map { student -> LoginStudentSelectItem(student, false) })
})
)
}
private fun registerStudents(students: List<Student>) {

View File

@ -1,7 +1,6 @@
package io.github.wulkanowy.ui.modules.luckynumber
import io.github.wulkanowy.data.repositories.luckynumber.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
@ -15,7 +14,6 @@ class LuckyNumberPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val luckyNumberRepository: LuckyNumberRepository,
private val semesterRepository: SemesterRepository,
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<LuckyNumberView>(errorHandler, studentRepository, schedulers) {
@ -38,7 +36,6 @@ class LuckyNumberPresenter @Inject constructor(
disposable.apply {
clear()
add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getCurrentSemester(it) }
.flatMapMaybe { luckyNumberRepository.getLuckyNumber(it, forceRefresh) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)

View File

@ -1,21 +1,14 @@
package io.github.wulkanowy.ui.modules.luckynumberwidget
import android.annotation.TargetApi
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_DELETED
import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED
import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_OPTIONS
import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT
import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH
import android.content.BroadcastReceiver
import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.RemoteViews
@ -25,7 +18,6 @@ import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.repositories.luckynumber.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
@ -34,14 +26,11 @@ import io.reactivex.Maybe
import timber.log.Timber
import javax.inject.Inject
class LuckyNumberWidgetProvider : BroadcastReceiver() {
class LuckyNumberWidgetProvider : AppWidgetProvider() {
@Inject
lateinit var studentRepository: StudentRepository
@Inject
lateinit var semesterRepository: SemesterRepository
@Inject
lateinit var luckyNumberRepository: LuckyNumberRepository
@ -59,20 +48,20 @@ class LuckyNumberWidgetProvider : BroadcastReceiver() {
fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId"
fun getThemeWidgetKey(appWidgetId: Int) = "lucky_number_widget_theme_$appWidgetId"
fun getHeightWidgetKey(appWidgetId: Int) = "lucky_number_widget_height_$appWidgetId"
fun getWidthWidgetKey(appWidgetId: Int) = "lucky_number_widget_width_$appWidgetId"
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
override fun onReceive(context: Context, intent: Intent) {
AndroidInjection.inject(this, context)
when (intent.action) {
ACTION_APPWIDGET_UPDATE -> onUpdate(context, intent)
ACTION_APPWIDGET_DELETED -> onDelete(intent)
ACTION_APPWIDGET_OPTIONS_CHANGED -> onOptionsChange(context, intent)
}
super.onReceive(context, intent)
}
private fun onUpdate(context: Context, intent: Intent) {
intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS)?.forEach { appWidgetId ->
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray?) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
appWidgetIds?.forEach { appWidgetId ->
val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0)
val layoutId = if (savedTheme == 0L) R.layout.widget_luckynumber else R.layout.widget_luckynumber_dark
@ -85,14 +74,64 @@ class LuckyNumberWidgetProvider : BroadcastReceiver() {
setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent)
}
setStyles(remoteView, intent)
setStyles(remoteView, appWidgetId)
appWidgetManager.updateAppWidget(appWidgetId, remoteView)
}
}
private fun onDelete(intent: Intent) {
val appWidgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID, 0)
if (appWidgetId != 0) sharedPref.delete(getStudentWidgetKey(appWidgetId))
override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
super.onDeleted(context, appWidgetIds)
appWidgetIds?.forEach { appWidgetId ->
if (appWidgetId != 0) sharedPref.delete(getStudentWidgetKey(appWidgetId))
}
}
override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle?) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0)
val layoutId = if (savedTheme == 0L) R.layout.widget_luckynumber else R.layout.widget_luckynumber_dark
val remoteView = RemoteViews(context.packageName, layoutId)
setStyles(remoteView, appWidgetId, newOptions)
appWidgetManager.updateAppWidget(appWidgetId, remoteView)
}
private fun setStyles(views: RemoteViews, appWidgetId: Int, options: Bundle? = null) {
val width = options?.getInt(OPTION_APPWIDGET_MIN_WIDTH) ?: sharedPref.getLong(getWidthWidgetKey(appWidgetId), 74).toInt()
val height = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: sharedPref.getLong(getHeightWidgetKey(appWidgetId), 74).toInt()
sharedPref.putLong(getWidthWidgetKey(appWidgetId), width.toLong())
sharedPref.putLong(getHeightWidgetKey(appWidgetId), height.toLong())
val rows = getCellsForSize(height)
val cols = getCellsForSize(width)
Timber.d("New lucky number widget measurement: %dx%d", width, height)
Timber.d("Widget size: $cols x $rows")
when {
1 == cols && 1 == rows -> views.setVisibility(imageTop = false, imageLeft = false)
1 == cols && 1 < rows -> views.setVisibility(imageTop = true, imageLeft = false)
1 < cols && 1 == rows -> views.setVisibility(imageTop = false, imageLeft = true)
1 == cols && 1 == rows -> views.setVisibility(imageTop = true, imageLeft = false)
2 == cols && 1 == rows -> views.setVisibility(imageTop = false, imageLeft = true)
else -> views.setVisibility(imageTop = false, imageLeft = false, title = true)
}
}
private fun RemoteViews.setVisibility(imageTop: Boolean, imageLeft: Boolean, title: Boolean = false) {
setViewVisibility(R.id.luckyNumberWidgetImageTop, if (imageTop) VISIBLE else GONE)
setViewVisibility(R.id.luckyNumberWidgetImageLeft, if (imageLeft) VISIBLE else GONE)
setViewVisibility(R.id.luckyNumberWidgetTitle, if (title) VISIBLE else GONE)
setViewVisibility(R.id.luckyNumberWidgetNumber, VISIBLE)
}
private fun getCellsForSize(size: Int): Int {
var n = 2
while (74 * n - 30 < size) ++n
return n - 1
}
private fun getLuckyNumber(studentId: Long, appWidgetId: Int): LuckyNumber? {
@ -113,7 +152,6 @@ class LuckyNumberWidgetProvider : BroadcastReceiver() {
else -> Maybe.empty()
}
}
.flatMap { semesterRepository.getCurrentSemester(it).toMaybe() }
.flatMap { luckyNumberRepository.getLuckyNumber(it) }
.subscribeOn(schedulers.backgroundThread)
.blockingGet()
@ -124,69 +162,4 @@ class LuckyNumberWidgetProvider : BroadcastReceiver() {
null
}
}
private fun onOptionsChange(context: Context, intent: Intent) {
intent.extras?.getInt(EXTRA_APPWIDGET_ID)?.let { appWidgetId ->
val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0)
val layoutId = if (savedTheme == 0L) R.layout.widget_luckynumber else R.layout.widget_luckynumber_dark
val remoteView = RemoteViews(context.packageName, layoutId)
setStyles(remoteView, intent)
appWidgetManager.updateAppWidget(appWidgetId, remoteView)
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private fun setStyles(views: RemoteViews, intent: Intent) {
val options = intent.extras?.getBundle(EXTRA_APPWIDGET_OPTIONS)
val maxWidth = options?.getInt(OPTION_APPWIDGET_MAX_WIDTH) ?: 150
val maxHeight = options?.getInt(OPTION_APPWIDGET_MAX_HEIGHT) ?: 40
Timber.d("New lucky number widget measurement: %dx%d", maxWidth, maxHeight)
when {
// 1x1
maxWidth < 150 && maxHeight < 110 -> {
Timber.d("Lucky number widget size: 1x1")
with(views) {
setViewVisibility(R.id.luckyNumberWidgetImageTop, GONE)
setViewVisibility(R.id.luckyNumberWidgetImageLeft, GONE)
setViewVisibility(R.id.luckyNumberWidgetTitle, GONE)
setViewVisibility(R.id.luckyNumberWidgetNumber, VISIBLE)
}
}
// 1x2
maxWidth < 150 && maxHeight > 110 -> {
Timber.d("Lucky number widget size: 1x2")
with(views) {
setViewVisibility(R.id.luckyNumberWidgetImageTop, VISIBLE)
setViewVisibility(R.id.luckyNumberWidgetImageLeft, GONE)
setViewVisibility(R.id.luckyNumberWidgetTitle, GONE)
setViewVisibility(R.id.luckyNumberWidgetNumber, VISIBLE)
}
}
// 2x1
maxWidth >= 150 && maxHeight <= 110 -> {
Timber.d("Lucky number widget size: 2x1")
with(views) {
setViewVisibility(R.id.luckyNumberWidgetImageTop, GONE)
setViewVisibility(R.id.luckyNumberWidgetImageLeft, VISIBLE)
setViewVisibility(R.id.luckyNumberWidgetTitle, GONE)
setViewVisibility(R.id.luckyNumberWidgetNumber, VISIBLE)
}
}
// 2x2 and bigger
else -> {
Timber.d("Lucky number widget size: 2x2 and bigger")
with(views) {
setViewVisibility(R.id.luckyNumberWidgetImageTop, GONE)
setViewVisibility(R.id.luckyNumberWidgetImageLeft, GONE)
setViewVisibility(R.id.luckyNumberWidgetTitle, VISIBLE)
setViewVisibility(R.id.luckyNumberWidgetNumber, VISIBLE)
}
}
}
}
}

View File

@ -68,6 +68,8 @@ class MainActivity : BaseActivity<MainPresenter>(), MainView {
override val currentViewTitle get() = (navController.currentFrag as? MainView.TitledView)?.titleStringId?.let { getString(it) }
override val currentViewSubtitle get() = (navController.currentFrag as? MainView.TitledView)?.subtitleString
override var startMenuIndex = 0
override var startMenuMoreIndex = -1
@ -152,6 +154,10 @@ class MainActivity : BaseActivity<MainPresenter>(), MainView {
supportActionBar?.title = title
}
override fun setViewSubTitle(subtitle: String?) {
supportActionBar?.subtitle = subtitle
}
override fun showHomeArrow(show: Boolean) {
supportActionBar?.setDisplayHomeAsUpEnabled(show)
}

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