1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-20 06:19:09 -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/dynamic.xml
.idea/uiDesigner.xml .idea/uiDesigner.xml
.idea/runConfigurations.xml .idea/runConfigurations.xml
.idea/discord.xml
# Keystore files # Keystore files
*.jks *.jks

View File

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

View File

@ -4,7 +4,7 @@ jdk: oraclejdk8
env: env:
global: global:
- ANDROID_API_LEVEL=29 - ANDROID_API_LEVEL=29
- ANDROID_BUILD_TOOLS_VERSION=29.0.2 - ANDROID_BUILD_TOOLS_VERSION=29.0.3
cache: cache:
directories: directories:
@ -14,7 +14,7 @@ cache:
branches: branches:
only: only:
- develop - develop
- 0.15.0 - 0.16.0
android: android:
licenses: licenses:
@ -49,9 +49,9 @@ script:
- ./gradlew dependencies --stacktrace --daemon - ./gradlew dependencies --stacktrace --daemon
- fossa --no-ansi || true - fossa --no-ansi || true
#- ./gradlew lintPlayRelease -x fabricGenerateResourcesPlayRelease --stacktrace --daemon #- ./gradlew lintPlayRelease -x fabricGenerateResourcesPlayRelease --stacktrace --daemon
- ./gradlew testPlayDebugUnitTest -x fabricGenerateResourcesPlay --stacktrace --daemon - ./gradlew -Pcoverage testPlayDebugUnitTest -x fabricGenerateResourcesPlay --stacktrace --daemon
- ./gradlew createFdroidDebugCoverageReport --stacktrace --daemon - ./gradlew -Pcoverage createFdroidDebugCoverageReport --stacktrace --daemon
- ./gradlew jacocoTestReport --stacktrace --daemon - ./gradlew -Pcoverage jacocoTestReport --stacktrace --daemon
- if [ -z ${SONAR_HOST+x} ]; then echo "sonar scan skipped"; else - if [ -z ${SONAR_HOST+x} ]; then echo "sonar scan skipped"; else
git fetch --unshallow; 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; ./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 { android {
compileSdkVersion 29 compileSdkVersion 29
buildToolsVersion '29.0.2' buildToolsVersion '29.0.3'
defaultConfig { defaultConfig {
applicationId "io.github.wulkanowy" applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 16 minSdkVersion 17
targetSdkVersion 29 targetSdkVersion 29
versionCode 52 versionCode 53
versionName "0.15.0" versionName "0.16.0"
multiDexEnabled true multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@ -61,7 +61,7 @@ android {
buildConfigField "boolean", "CRASHLYTICS_ENABLED", project.hasProperty("enableCrashlytics") ? "true" : "false" buildConfigField "boolean", "CRASHLYTICS_ENABLED", project.hasProperty("enableCrashlytics") ? "true" : "false"
applicationIdSuffix ".dev" applicationIdSuffix ".dev"
versionNameSuffix "-dev" versionNameSuffix "-dev"
testCoverageEnabled = true testCoverageEnabled = project.hasProperty('coverage')
ext.enableCrashlytics = project.hasProperty("enableCrashlytics") ext.enableCrashlytics = project.hasProperty("enableCrashlytics")
} }
} }
@ -110,9 +110,9 @@ play {
} }
ext { ext {
work_manager = "2.3.0" work_manager = "2.3.2"
room = "2.2.3" room = "2.2.4"
dagger = "2.25.4" dagger = "2.26"
chucker = "2.0.4" chucker = "2.0.4"
mockk = "1.9.2" mockk = "1.9.2"
} }
@ -122,14 +122,14 @@ configurations.all {
} }
dependencies { 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 "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.activity:activity-ktx:1.1.0"
implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.appcompat:appcompat-resources: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.annotation:annotation:1.1.0"
implementation "androidx.multidex:multidex:2.0.1" implementation "androidx.multidex:multidex:2.0.1"
@ -139,7 +139,7 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03"
implementation "androidx.constraintlayout:constraintlayout:1.1.3" implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0" 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.wulkanowy:material-chips-input:2.0.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation "me.zhanghai.android.materialprogressbar:library:1.6.1" implementation "me.zhanghai.android.materialprogressbar:library:1.6.1"
@ -167,18 +167,19 @@ dependencies {
implementation "com.github.pwittchen:reactivenetwork-rx2:3.0.6" implementation "com.github.pwittchen:reactivenetwork-rx2:3.0.6"
implementation "io.reactivex.rxjava2:rxandroid:2.1.1" 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.google.code.gson:gson:2.8.6"
implementation "com.jakewharton.threetenabp:threetenabp:1.2.2" implementation "com.jakewharton.threetenabp:threetenabp:1.2.2"
implementation "com.jakewharton.timber:timber:4.7.1" implementation "com.jakewharton.timber:timber:4.7.1"
implementation "at.favre.lib:slf4j-timber:1.0.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.mikepenz:aboutlibraries-core:7.1.0"
implementation 'com.wdullaer:materialdatetimepicker:4.2.3' 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" playImplementation "com.crashlytics.sdk.android:crashlytics:2.10.1"
releaseImplementation "fr.o80.chucker:library-no-op:$chucker" releaseImplementation "fr.o80.chucker:library-no-op:$chucker"
@ -189,7 +190,7 @@ dependencies {
testImplementation "junit:junit:4.13" testImplementation "junit:junit:4.13"
testImplementation "io.mockk:mockk:$mockk" testImplementation "io.mockk:mockk:$mockk"
testImplementation "org.threeten:threetenbp:1.4.1" 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:core:1.2.0"
androidTestImplementation "androidx.test:runner:1.2.0" androidTestImplementation "androidx.test:runner:1.2.0"
@ -197,7 +198,7 @@ dependencies {
androidTestImplementation "io.mockk:mockk-android:$mockk" androidTestImplementation "io.mockk:mockk-android:$mockk"
androidTestImplementation "androidx.room:room-testing:$room" androidTestImplementation "androidx.room:room-testing:$room"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 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' 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.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.of import org.threeten.bp.LocalDate.of
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -104,45 +103,45 @@ class Migration13Test : AbstractMigrationTest() {
val db = helper.runMigrationsAndValidate(dbName, 13, true, Migration13()) val db = helper.runMigrationsAndValidate(dbName, 13, true, Migration13())
val semesters1 = getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 1 AND class_id = 5") 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 { semesters1[0].run {
assertFalse(isCurrent) assertFalse(second)
assertEquals(1, semesterId) assertEquals(1, first.semesterId)
assertEquals(1, diaryId) assertEquals(1, first.diaryId)
} }
semesters1[2].run { semesters1[2].run {
assertFalse(isCurrent) assertFalse(second)
assertEquals(3, semesterId) assertEquals(3, first.semesterId)
assertEquals(2, diaryId) assertEquals(2, first.diaryId)
} }
semesters1[3].run { semesters1[3].run {
assertTrue(isCurrent) assertTrue(second)
assertEquals(4, semesterId) assertEquals(4, first.semesterId)
assertEquals(2, diaryId) assertEquals(2, first.diaryId)
} }
getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let { getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let {
assertTrue { it.single { it.isCurrent }.isCurrent } assertTrue { it.single { it.second }.second }
assertEquals(1970, it[0].schoolYear) assertEquals(1970, it[0].first.schoolYear)
assertEquals(of(1970, 1, 1), it[0].end) assertEquals(of(1970, 1, 1), it[0].first.end)
assertEquals(of(1970, 1, 1), it[0].start) assertEquals(of(1970, 1, 1), it[0].first.start)
assertFalse(it[0].isCurrent) assertFalse(it[0].second)
assertFalse(it[1].isCurrent) assertFalse(it[1].second)
assertFalse(it[2].isCurrent) assertFalse(it[2].second)
assertTrue(it[3].isCurrent) assertTrue(it[3].second)
} }
getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let { getSemesters(db, "SELECT * FROM Semesters WHERE student_id = 2 AND class_id = 5").let {
assertTrue { it.single { it.isCurrent }.isCurrent } assertTrue { it.single { it.second }.second }
assertFalse(it[0].isCurrent) assertFalse(it[0].second)
assertFalse(it[1].isCurrent) assertFalse(it[1].second)
assertFalse(it[2].isCurrent) assertFalse(it[2].second)
assertTrue(it[3].isCurrent) assertTrue(it[3].second)
} }
} }
private fun getSemesters(db: SupportSQLiteDatabase, query: String): List<Semester> { private fun getSemesters(db: SupportSQLiteDatabase, query: String): List<Pair<Semester, Boolean>> {
val semesters = mutableListOf<Semester>() val semesters = mutableListOf<Pair<Semester, Boolean>>()
val cursor = db.query(query) val cursor = db.query(query)
if (cursor.moveToFirst()) { if (cursor.moveToFirst()) {
@ -153,13 +152,12 @@ class Migration13Test : AbstractMigrationTest() {
diaryName = cursor.getString(3), diaryName = cursor.getString(3),
semesterId = cursor.getInt(4), semesterId = cursor.getInt(4),
semesterName = cursor.getInt(5), semesterName = cursor.getInt(5),
isCurrent = cursor.getInt(6) == 1,
classId = cursor.getInt(7), classId = cursor.getInt(7),
unitId = cursor.getInt(8), unitId = cursor.getInt(8),
schoolYear = cursor.getInt(9), schoolYear = cursor.getInt(9),
start = Converters().timestampToDate(cursor.getLong(10))!!, start = Converters().timestampToDate(cursor.getLong(10))!!,
end = Converters().timestampToDate(cursor.getLong(11))!! end = Converters().timestampToDate(cursor.getLong(11))!!
)) ) to (cursor.getInt(6) == 1))
} while (cursor.moveToNext()) } while (cursor.moveToNext())
} }
return semesters.toList() return semesters.toList()

View File

@ -10,8 +10,8 @@ import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.of
import kotlin.test.assertEquals import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@ -35,19 +35,19 @@ class AttendanceLocalTest {
@Test @Test
fun saveAndReadTest() { fun saveAndReadTest() {
attendanceLocal.saveAttendance(listOf( 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, 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, 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, 17), 0, "", "", false, false, false, false, false, false, false, SentExcuseStatus.ACCEPTED.name)
)) ))
val attendance = attendanceLocal val attendance = attendanceLocal
.getAttendance(Semester(1, 2, "", 1, 3, 2019, true, now(), now(), 1, 1), .getAttendance(Semester(1, 2, "", 1, 3, 2019, now(), now(), 1, 1),
LocalDate.of(2018, 9, 10), of(2018, 9, 10),
LocalDate.of(2018, 9, 14) of(2018, 9, 14)
) )
.blockingGet() .blockingGet()
assertEquals(2, attendance.size) assertEquals(2, attendance.size)
assertEquals(attendance[0].date, LocalDate.of(2018, 9, 10)) assertEquals(attendance[0].date, of(2018, 9, 10))
assertEquals(attendance[1].date, LocalDate.of(2018, 9, 14)) 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.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now
import org.threeten.bp.LocalDate.of
import kotlin.test.assertEquals import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@ -35,20 +37,20 @@ class CompletedLessonsLocalTest {
@Test @Test
fun saveAndReadTest() { fun saveAndReadTest() {
completedLessonsLocal.saveCompletedLessons(listOf( completedLessonsLocal.saveCompletedLessons(listOf(
getCompletedLesson(LocalDate.of(2018, 9, 10), 1), getCompletedLesson(of(2018, 9, 10), 1),
getCompletedLesson(LocalDate.of(2018, 9, 14), 2), getCompletedLesson(of(2018, 9, 14), 2),
getCompletedLesson(LocalDate.of(2018, 9, 17), 3) getCompletedLesson(of(2018, 9, 17), 3)
)) ))
val completed = completedLessonsLocal val completed = completedLessonsLocal
.getCompletedLessons(Semester(1, 2, "", 1, 3, 2019, true, LocalDate.now(), LocalDate.now(), 1, 1), .getCompletedLessons(Semester(1, 2, "", 1, 3, 2019, now(), now(), 1, 1),
LocalDate.of(2018, 9, 10), of(2018, 9, 10),
LocalDate.of(2018, 9, 14) of(2018, 9, 14)
) )
.blockingGet() .blockingGet()
assertEquals(2, completed.size) assertEquals(2, completed.size)
assertEquals(completed[0].date, LocalDate.of(2018, 9, 10)) assertEquals(completed[0].date, of(2018, 9, 10))
assertEquals(completed[1].date, LocalDate.of(2018, 9, 14)) assertEquals(completed[1].date, of(2018, 9, 14))
} }
private fun getCompletedLesson(date: LocalDate, number: Int): CompletedLesson { 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.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith 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 import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@ -34,19 +35,19 @@ class ExamLocalTest {
@Test @Test
fun saveAndReadTest() { fun saveAndReadTest() {
examLocal.saveExams(listOf( examLocal.saveExams(listOf(
Exam(1, 2, LocalDate.of(2018, 9, 10), LocalDate.now(), "", "", "", "", "", ""), Exam(1, 2, of(2018, 9, 10), now(), "", "", "", "", "", ""),
Exam(1, 2, LocalDate.of(2018, 9, 14), LocalDate.now(), "", "", "", "", "", ""), Exam(1, 2, of(2018, 9, 14), now(), "", "", "", "", "", ""),
Exam(1, 2, LocalDate.of(2018, 9, 17), LocalDate.now(), "", "", "", "", "", "") Exam(1, 2, of(2018, 9, 17), now(), "", "", "", "", "", "")
)) ))
val exams = examLocal val exams = examLocal
.getExams(Semester(1, 2, "", 1, 3, 2019, true, LocalDate.now(), LocalDate.now(), 1, 1), .getExams(Semester(1, 2, "", 1, 3, 2019, now(), now(), 1, 1),
LocalDate.of(2018, 9, 10), of(2018, 9, 10),
LocalDate.of(2018, 9, 14) of(2018, 9, 14)
) )
.blockingGet() .blockingGet()
assertEquals(2, exams.size) assertEquals(2, exams.size)
assertEquals(exams[0].date, LocalDate.of(2018, 9, 10)) assertEquals(exams[0].date, of(2018, 9, 10))
assertEquals(exams[1].date, LocalDate.of(2018, 9, 14)) 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) 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 val grades = gradeLocal
.getGrades(semester) .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.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress import androidx.test.filters.SdkSuppress
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings 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.AppDatabase
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student 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.MockKAnnotations
import io.mockk.every import io.mockk.every
import io.mockk.impl.annotations.MockK 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 io.reactivex.Single
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before

View File

@ -11,7 +11,7 @@ import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate.now
import kotlin.test.assertEquals import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@ -40,10 +40,7 @@ class GradeStatisticsLocalTest {
getGradeStatistics("Fizyka", 1, 2) getGradeStatistics("Fizyka", 1, 2)
)) ))
val stats = gradeStatisticsLocal.getGradesStatistics( val stats = gradeStatisticsLocal.getGradesStatistics(getSemester(), false, "Matematyka").blockingGet()
Semester(2, 2, "", 2019, 1, 2, true, LocalDate.now(), LocalDate.now(), 1, 1), false,
"Matematyka"
).blockingGet()
assertEquals(1, stats.size) assertEquals(1, stats.size)
assertEquals(stats[0].subject, "Matematyka") assertEquals(stats[0].subject, "Matematyka")
} }
@ -56,12 +53,11 @@ class GradeStatisticsLocalTest {
getGradeStatistics("Fizyka", 1, 2) getGradeStatistics("Fizyka", 1, 2)
)) ))
val stats = gradeStatisticsLocal.getGradesStatistics( val stats = gradeStatisticsLocal.getGradesStatistics(getSemester(), false, "Wszystkie").blockingGet()
Semester(2, 2, "", 2019, 1, 2, true, LocalDate.now(), LocalDate.now(), 1, 1), false, assertEquals(3, stats.size)
"Wszystkie"
).blockingGet()
assertEquals(1, stats.size)
assertEquals(stats[0].subject, "Wszystkie") assertEquals(stats[0].subject, "Wszystkie")
assertEquals(stats[1].subject, "Matematyka")
assertEquals(stats[2].subject, "Chemia")
} }
@Test @Test
@ -72,11 +68,8 @@ class GradeStatisticsLocalTest {
getGradePointsStatistics("Fizyka", 1, 2) getGradePointsStatistics("Fizyka", 1, 2)
)) ))
val stats = gradeStatisticsLocal.getGradesPointsStatistics( val stats = gradeStatisticsLocal.getGradesPointsStatistics(getSemester(), "Matematyka").blockingGet()
Semester(2, 2, "", 2019, 1, 2, true, LocalDate.now(), LocalDate.now(), 1, 1), with(stats[0]) {
"Matematyka"
).blockingGet()
with(stats) {
assertEquals(subject, "Matematyka") assertEquals(subject, "Matematyka")
assertEquals(others, 5.0) assertEquals(others, 5.0)
assertEquals(student, 5.0) assertEquals(student, 5.0)
@ -87,10 +80,7 @@ class GradeStatisticsLocalTest {
fun saveAndRead_subjectEmpty() { fun saveAndRead_subjectEmpty() {
gradeStatisticsLocal.saveGradesPointsStatistics(listOf()) gradeStatisticsLocal.saveGradesPointsStatistics(listOf())
val stats = gradeStatisticsLocal.getGradesPointsStatistics( val stats = gradeStatisticsLocal.getGradesPointsStatistics(getSemester(), "Matematyka").blockingGet()
Semester(2, 2, "", 2019, 1, 2, true, LocalDate.now(), LocalDate.now(), 1, 1),
"Matematyka"
).blockingGet()
assertEquals(null, stats) assertEquals(null, stats)
} }
@ -98,13 +88,14 @@ class GradeStatisticsLocalTest {
fun saveAndRead_allEmpty() { fun saveAndRead_allEmpty() {
gradeStatisticsLocal.saveGradesPointsStatistics(listOf()) gradeStatisticsLocal.saveGradesPointsStatistics(listOf())
val stats = gradeStatisticsLocal.getGradesPointsStatistics( val stats = gradeStatisticsLocal.getGradesPointsStatistics(getSemester(), "Wszystkie").blockingGet()
Semester(2, 2, "", 2019, 1, 2, true, LocalDate.now(), LocalDate.now(), 1, 1),
"Wszystkie"
).blockingGet()
assertEquals(null, stats) 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 { private fun getGradeStatistics(subject: String, studentId: Int, semesterId: Int): GradeStatistics {
return GradeStatistics(studentId, semesterId, subject, 5, 5, false) 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.AppDatabase
import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime.now
import kotlin.test.assertEquals import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@ -36,7 +38,7 @@ class LuckyNumberLocalTest {
fun saveAndReadTest() { fun saveAndReadTest() {
luckyNumberLocal.saveLuckyNumber(LuckyNumber(1, LocalDate.of(2019, 1, 20), 14)) 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) LocalDate.of(2019, 1, 20)
).blockingGet() ).blockingGet()

View File

@ -42,7 +42,7 @@ class RecipientLocalTest {
)) ))
val recipients = recipientLocal.getRecipients( 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, 2,
ReportingUnit(1, 4, "", 0, "", emptyList()) ReportingUnit(1, 4, "", 0, "", emptyList())
).blockingGet() ).blockingGet()

View File

@ -39,7 +39,7 @@ class StudentLocalTest {
@Test @Test
fun saveAndReadTest() { 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() .blockingGet()
val student = studentLocal.getCurrentStudent(true).blockingGet() val student = studentLocal.getCurrentStudent(true).blockingGet()

View File

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

View File

@ -41,7 +41,7 @@ class TimetableLocalTest {
)) ))
val exams = timetableDb.getTimetable( 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, 10),
LocalDate.of(2018, 9, 14) LocalDate.of(2018, 9, 14)
).blockingGet() ).blockingGet()

View File

@ -5,13 +5,12 @@ package io.github.wulkanowy.utils
import android.content.Context import android.content.Context
import timber.log.Timber import timber.log.Timber
fun initCrashlytics(context: Context, appInfo: AppInfo) { fun initCrashlytics(context: Context, appInfo: AppInfo) {}
// do nothing
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?) { class CrashlyticsExceptionTree : TimberTreeNoOp()
// do nothing
}
}

View File

@ -43,8 +43,8 @@
android:name=".ui.modules.message.send.SendMessageActivity" android:name=".ui.modules.message.send.SendMessageActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:label="@string/send_message_title" android:label="@string/send_message_title"
android:windowSoftInputMode="adjustResize" android:theme="@style/WulkanowyTheme.NoActionBar"
android:theme="@style/WulkanowyTheme.NoActionBar" /> android:windowSoftInputMode="adjustResize" />
<activity <activity
android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity" android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
@ -96,6 +96,16 @@
android:exported="false" android:exported="false"
tools:node="remove" /> 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 <meta-data
android:name="io.fabric.ApiKey" android:name="io.fabric.ApiKey"
android:value="${fabric_api_key}" /> android:value="${fabric_api_key}" />

View File

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

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy package io.github.wulkanowy
import android.content.Context import android.content.Context
import android.util.Log.DEBUG
import android.util.Log.INFO import android.util.Log.INFO
import android.util.Log.VERBOSE import android.util.Log.VERBOSE
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
@ -11,11 +12,13 @@ import dagger.android.AndroidInjector
import dagger.android.support.DaggerApplication import dagger.android.support.DaggerApplication
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.utils.Log import eu.davidea.flexibleadapter.utils.Log
import fr.bipi.tressence.file.FileLoggerTree
import io.github.wulkanowy.di.DaggerAppComponent import io.github.wulkanowy.di.DaggerAppComponent
import io.github.wulkanowy.services.sync.SyncWorkerFactory import io.github.wulkanowy.services.sync.SyncWorkerFactory
import io.github.wulkanowy.ui.base.ThemeManager import io.github.wulkanowy.ui.base.ThemeManager
import io.github.wulkanowy.utils.ActivityLifecycleLogger import io.github.wulkanowy.utils.ActivityLifecycleLogger
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.CrashlyticsExceptionTree
import io.github.wulkanowy.utils.CrashlyticsTree import io.github.wulkanowy.utils.CrashlyticsTree
import io.github.wulkanowy.utils.DebugLogTree import io.github.wulkanowy.utils.DebugLogTree
import io.github.wulkanowy.utils.initCrashlytics import io.github.wulkanowy.utils.initCrashlytics
@ -54,9 +57,17 @@ class WulkanowyApp : DaggerApplication(), Configuration.Provider {
private fun initLogging() { private fun initLogging() {
if (appInfo.isDebug) { if (appInfo.isDebug) {
Timber.plant(DebugLogTree())
FlexibleAdapter.enableLogs(Log.Level.DEBUG) 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 { } else {
Timber.plant(CrashlyticsExceptionTree())
Timber.plant(CrashlyticsTree()) Timber.plant(CrashlyticsTree())
} }
registerActivityLifecycleCallbacks(ActivityLifecycleLogger()) 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.Migration2
import io.github.wulkanowy.data.db.migrations.Migration20 import io.github.wulkanowy.data.db.migrations.Migration20
import io.github.wulkanowy.data.db.migrations.Migration21 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.Migration3
import io.github.wulkanowy.data.db.migrations.Migration4 import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration5
@ -103,7 +104,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 21 const val VERSION_SCHEMA = 22
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> { fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> {
return arrayOf( return arrayOf(
@ -126,7 +127,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration18(), Migration18(),
Migration19(sharedPrefProvider), Migration19(sharedPrefProvider),
Migration20(), Migration20(),
Migration21() Migration21(),
Migration22()
) )
} }

View File

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

View File

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

View File

@ -27,9 +27,6 @@ data class Semester(
@ColumnInfo(name = "semester_name") @ColumnInfo(name = "semester_name")
val semesterName: Int, val semesterName: Int,
@ColumnInfo(name = "is_current")
val isCurrent: Boolean,
val start: LocalDate, val start: LocalDate,
val end: LocalDate, val end: LocalDate,
@ -43,4 +40,8 @@ data class Semester(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
var id: Long = 0 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") @ColumnInfo(name = "school_id")
val schoolSymbol: String, val schoolSymbol: String,
@ColumnInfo(name ="school_short")
val schoolShortName: String,
@ColumnInfo(name = "school_name") @ColumnInfo(name = "school_name")
val schoolName: String, val schoolName: String,

View File

@ -41,7 +41,7 @@ data class Timetable(
val info: String, val info: String,
@ColumnInfo(name = "student_plan") @ColumnInfo(name = "student_plan")
val studentPlan: Boolean, val isStudentPlan: Boolean,
val changes: 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>> { fun getAppCreators(): Single<List<AppCreator>> {
return Single.fromCallable<List<AppCreator>> { return Single.fromCallable<List<AppCreator>> {
Gson().fromJson( Gson().fromJson(
assets.open("creators.json").bufferedReader().use { it.readText() }, assets.open("contributors.json").bufferedReader().use { it.readText() },
Array<AppCreator>::class.java Array<AppCreator>::class.java
).toList() ).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.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeStatistics import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.utils.roundToDecimalPlaces
import io.reactivex.Maybe import io.reactivex.Maybe
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -30,23 +29,17 @@ class GradeStatisticsLocal @Inject constructor(
list.groupBy { it.grade }.map { list.groupBy { it.grade }.map {
GradeStatistics(semester.studentId, semester.semesterId, subjectName, it.key, GradeStatistics(semester.studentId, semester.semesterId, subjectName, it.key,
it.value.fold(0) { acc, e -> acc + e.amount }, false) it.value.fold(0) { acc, e -> acc + e.amount }, false)
} } + list
} }
else -> gradeStatisticsDb.loadSubject(semester.semesterId, semester.studentId, subjectName, isSemester) else -> gradeStatisticsDb.loadSubject(semester.semesterId, semester.studentId, subjectName, isSemester)
}.filter { it.isNotEmpty() } }.filter { it.isNotEmpty() }
} }
fun getGradesPointsStatistics(semester: Semester, subjectName: String): Maybe<GradePointsStatistics> { fun getGradesPointsStatistics(semester: Semester, subjectName: String): Maybe<List<GradePointsStatistics>> {
return when (subjectName) { return when (subjectName) {
"Wszystkie" -> gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId).flatMap { list -> "Wszystkie" -> gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId)
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)
))
}
else -> gradePointsStatisticsDb.loadSubject(semester.semesterId, semester.studentId, subjectName) else -> gradePointsStatisticsDb.loadSubject(semester.semesterId, semester.studentId, subjectName)
} }.filter { it.isNotEmpty() }
} }
fun saveGradesStatistics(gradesStatistics: List<GradeStatistics>) { 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.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeStatistics import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.data.db.entities.Semester 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.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Maybe
import io.reactivex.Single import io.reactivex.Single
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
@ -19,8 +20,8 @@ class GradeStatisticsRepository @Inject constructor(
private val remote: GradeStatisticsRemote private val remote: GradeStatisticsRemote
) { ) {
fun getGradesStatistics(semester: Semester, subjectName: String, isSemester: Boolean, forceRefresh: Boolean = false): Single<List<GradeStatistics>> { fun getGradesStatistics(semester: Semester, subjectName: String, isSemester: Boolean, forceRefresh: Boolean = false): Single<List<GradeStatisticsItem>> {
return local.getGradesStatistics(semester, isSemester, subjectName).filter { !forceRefresh } return local.getGradesStatistics(semester, isSemester, subjectName).map { it.mapToStatisticItems() }.filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap { .flatMap {
if (it) remote.getGradeStatistics(semester, isSemester) if (it) remote.getGradeStatistics(semester, isSemester)
@ -31,21 +32,43 @@ class GradeStatisticsRepository @Inject constructor(
local.deleteGradesStatistics(old.uniqueSubtract(new)) local.deleteGradesStatistics(old.uniqueSubtract(new))
local.saveGradesStatistics(new.uniqueSubtract(old)) 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> { fun getGradesPointsStatistics(semester: Semester, subjectName: String, forceRefresh: Boolean): Single<List<GradeStatisticsItem>> {
return local.getGradesPointsStatistics(semester, subjectName).filter { !forceRefresh } return local.getGradesPointsStatistics(semester, subjectName).map { it.mapToStatisticsItem() }.filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMapMaybe { .flatMap {
if (it) remote.getGradePointsStatistics(semester).toMaybe() if (it) remote.getGradePointsStatistics(semester)
else Maybe.error(UnknownHostException()) else Single.error(UnknownHostException())
}.flatMap { new -> }.flatMap { new ->
local.getGradesPointsStatistics(semester).defaultIfEmpty(emptyList()) local.getGradesPointsStatistics(semester).toSingle(emptyList())
.doOnSuccess { old -> .doOnSuccess { old ->
local.deleteGradesPointsStatistics(old.uniqueSubtract(new)) local.deleteGradesPointsStatistics(old.uniqueSubtract(new))
local.saveGradesPointsStatistics(new.uniqueSubtract(old)) 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.dao.LuckyNumberDao
import io.github.wulkanowy.data.db.entities.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.reactivex.Maybe import io.reactivex.Maybe
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import javax.inject.Inject import javax.inject.Inject
@ -23,7 +23,7 @@ class LuckyNumberLocal @Inject constructor(private val luckyNumberDb: LuckyNumbe
luckyNumberDb.deleteAll(listOf(luckyNumber)) luckyNumberDb.deleteAll(listOf(luckyNumber))
} }
fun getLuckyNumber(semester: Semester, date: LocalDate): Maybe<LuckyNumber> { fun getLuckyNumber(student: Student, date: LocalDate): Maybe<LuckyNumber> {
return luckyNumberDb.load(semester.studentId, date) return luckyNumberDb.load(student.studentId, date)
} }
} }

View File

@ -1,7 +1,7 @@
package io.github.wulkanowy.data.repositories.luckynumber package io.github.wulkanowy.data.repositories.luckynumber
import io.github.wulkanowy.data.db.entities.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.github.wulkanowy.sdk.Sdk
import io.reactivex.Maybe import io.reactivex.Maybe
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
@ -11,11 +11,10 @@ import javax.inject.Singleton
@Singleton @Singleton
class LuckyNumberRemote @Inject constructor(private val sdk: Sdk) { class LuckyNumberRemote @Inject constructor(private val sdk: Sdk) {
fun getLuckyNumber(semester: Semester): Maybe<LuckyNumber> { fun getLuckyNumber(student: Student): Maybe<LuckyNumber> {
return sdk.getLuckyNumber() return sdk.getLuckyNumber(student.schoolShortName).map {
.map {
LuckyNumber( LuckyNumber(
studentId = semester.studentId, studentId = student.studentId,
date = LocalDate.now(), date = LocalDate.now(),
luckyNumber = it 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.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings 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.LuckyNumber
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Maybe import io.reactivex.Maybe
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
@ -18,14 +18,14 @@ class LuckyNumberRepository @Inject constructor(
private val remote: LuckyNumberRemote private val remote: LuckyNumberRemote
) { ) {
fun getLuckyNumber(semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Maybe<LuckyNumber> { fun getLuckyNumber(student: Student, forceRefresh: Boolean = false, notify: Boolean = false): Maybe<LuckyNumber> {
return local.getLuckyNumber(semester, LocalDate.now()).filter { !forceRefresh } return local.getLuckyNumber(student, LocalDate.now()).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMapMaybe { .flatMapMaybe {
if (it) remote.getLuckyNumber(semester) if (it) remote.getLuckyNumber(student)
else Maybe.error(UnknownHostException()) else Maybe.error(UnknownHostException())
}.flatMap { new -> }.flatMap { new ->
local.getLuckyNumber(semester, LocalDate.now()) local.getLuckyNumber(student, LocalDate.now())
.doOnSuccess { old -> .doOnSuccess { old ->
if (new != old) { if (new != old) {
local.deleteLuckyNumber(old) local.deleteLuckyNumber(old)
@ -39,13 +39,13 @@ class LuckyNumberRepository @Inject constructor(
if (notify) isNotified = false if (notify) isNotified = false
}) })
} }
}.flatMap({ local.getLuckyNumber(semester, LocalDate.now()) }, { Maybe.error(it) }, }.flatMap({ local.getLuckyNumber(student, LocalDate.now()) }, { Maybe.error(it) },
{ local.getLuckyNumber(semester, LocalDate.now()) }) { local.getLuckyNumber(student, LocalDate.now()) })
) )
} }
fun getNotNotifiedLuckyNumber(semester: Semester): Maybe<LuckyNumber> { fun getNotNotifiedLuckyNumber(student: Student): Maybe<LuckyNumber> {
return local.getLuckyNumber(semester, LocalDate.now()).filter { !it.isNotified } return local.getLuckyNumber(student, LocalDate.now()).filter { !it.isNotified }
} }
fun updateLuckyNumber(luckyNumber: LuckyNumber): Completable { 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.db.entities.Student
import io.github.wulkanowy.data.repositories.message.MessageFolder.TRASHED import io.github.wulkanowy.data.repositories.message.MessageFolder.TRASHED
import io.reactivex.Maybe import io.reactivex.Maybe
import io.reactivex.Single
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -23,7 +24,7 @@ class MessageLocal @Inject constructor(private val messagesDb: MessagesDao) {
messagesDb.deleteAll(messages) messagesDb.deleteAll(messages)
} }
fun getMessage(id: Long): Maybe<Message> { fun getMessage(id: Long): Single<Message> {
return messagesDb.load(id) return messagesDb.load(id)
} }

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Maybe import io.reactivex.Maybe
import io.reactivex.Single import io.reactivex.Single
import timber.log.Timber
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -51,21 +52,26 @@ class MessageRepository @Inject constructor(
return Single.just(sdkHelper.init(student)) return Single.just(sdkHelper.init(student))
.flatMap { _ -> .flatMap { _ ->
local.getMessage(messageDbId) 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) .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap { .flatMap {
if (it) local.getMessage(messageDbId).toSingle() if (it) local.getMessage(messageDbId)
else Single.error(UnknownHostException()) else Single.error(UnknownHostException())
} }
.flatMap { dbMessage -> .flatMap { dbMessage ->
remote.getMessagesContent(dbMessage, markAsRead).doOnSuccess { remote.getMessagesContent(dbMessage, markAsRead).doOnSuccess {
local.updateMessages(listOf(dbMessage.copy(unread = false).apply { local.updateMessages(listOf(dbMessage.copy(unread = !markAsRead).apply {
id = dbMessage.id id = dbMessage.id
content = content.ifBlank { it } content = content.ifBlank { it }
})) }))
Timber.d("Message $messageDbId with blank content: ${dbMessage.content.isBlank()}, marked as read")
} }
}.flatMap { }.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>> { 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, schoolYear = it.schoolYear,
semesterId = it.semesterId, semesterId = it.semesterId,
semesterName = it.semesterNumber, semesterName = it.semesterNumber,
isCurrent = it.current,
start = it.start, start = it.start,
end = it.end, end = it.end,
classId = it.classId, 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.SdkHelper
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student 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.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Maybe import io.reactivex.Maybe
import io.reactivex.Single import io.reactivex.Single
import timber.log.Timber
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -21,28 +22,30 @@ class SemesterRepository @Inject constructor(
private val sdkHelper: SdkHelper 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)) 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) .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap { .flatMap {
if (it) remote.getSemesters(student) else Single.error(UnknownHostException()) if (it) remote.getSemesters(student)
else Single.error(UnknownHostException())
}.flatMap { new -> }.flatMap { new ->
val currentSemesters = new.filter { it.isCurrent } if (new.isEmpty()) throw IllegalArgumentException("Empty semester list!")
if (currentSemesters.size == 1) {
local.getSemesters(student).toSingle(emptyList()) local.getSemesters(student).toSingle(emptyList()).doOnSuccess { old ->
.doOnSuccess { old ->
local.deleteSemesters(old.uniqueSubtract(new)) local.deleteSemesters(old.uniqueSubtract(new))
local.saveSemesters(new.uniqueSubtract(old)) local.saveSemesters(new.uniqueSubtract(old))
} }
} else {
Timber.i("Current semesters list:\n${new.joinToString(separator = "\n")}")
throw IllegalArgumentException("Current semester can be only one.")
}
}.flatMap { local.getSemesters(student).toSingle(emptyList()) }) }.flatMap { local.getSemesters(student).toSingle(emptyList()) })
} }
fun getCurrentSemester(student: Student, forceRefresh: Boolean = false): Single<Semester> { 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, userLoginId = student.userLoginId,
studentName = student.studentName, studentName = student.studentName,
schoolSymbol = student.schoolSymbol, schoolSymbol = student.schoolSymbol,
schoolShortName = student.schoolShortName,
schoolName = student.schoolName, schoolName = student.schoolName,
className = student.className, className = student.className,
classId = student.classId, 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 { return ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getStudentsScrapper(email, password, endpoint, symbol) if (it) remote.getStudentsScrapper(email, password, endpoint, symbol)
else Single.error(UnknownHostException("No internet connection")) else Single.error(UnknownHostException("No internet connection"))

View File

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

View File

@ -9,6 +9,12 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
import dagger.multibindings.IntoSet 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.AttendanceSummaryWork
import io.github.wulkanowy.services.sync.works.AttendanceWork import io.github.wulkanowy.services.sync.works.AttendanceWork
import io.github.wulkanowy.services.sync.works.CompletedLessonWork import io.github.wulkanowy.services.sync.works.CompletedLessonWork
@ -29,11 +35,10 @@ import javax.inject.Singleton
@Suppress("unused") @Suppress("unused")
@AssistedModule @AssistedModule
@Module(includes = [AssistedInject_ServicesModule::class, ServicesModule.Static::class]) @Module(includes = [AssistedInject_ServicesModule::class])
abstract class ServicesModule { abstract class ServicesModule {
@Module companion object {
object Static {
@Provides @Provides
fun provideWorkManager(context: Context) = WorkManager.getInstance(context) fun provideWorkManager(context: Context) = WorkManager.getInstance(context)
@ -101,4 +106,24 @@ abstract class ServicesModule {
@Binds @Binds
@IntoSet @IntoSet
abstract fun provideGradeStatistics(work: GradeStatisticsWork): Work 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.SDK_INT
import android.os.Build.VERSION_CODES.O import android.os.Build.VERSION_CODES.O
import androidx.core.app.NotificationManagerCompat
import androidx.work.BackoffPolicy.EXPONENTIAL import androidx.work.BackoffPolicy.EXPONENTIAL
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy.KEEP 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
import io.github.wulkanowy.data.db.SharedPrefProvider.Companion.APP_VERSION_CODE_KEY import io.github.wulkanowy.data.db.SharedPrefProvider.Companion.APP_VERSION_CODE_KEY
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.DebugChannel import io.github.wulkanowy.services.sync.channels.Channel
import io.github.wulkanowy.services.sync.channels.NewEntriesChannel
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.isHolidays import io.github.wulkanowy.utils.isHolidays
import org.threeten.bp.LocalDate.now import org.threeten.bp.LocalDate.now
@ -27,9 +27,9 @@ import javax.inject.Singleton
class SyncManager @Inject constructor( class SyncManager @Inject constructor(
private val workManager: WorkManager, private val workManager: WorkManager,
private val preferencesRepository: PreferencesRepository, private val preferencesRepository: PreferencesRepository,
channels: Set<@JvmSuppressWildcards Channel>,
notificationManager: NotificationManagerCompat,
sharedPrefProvider: SharedPrefProvider, sharedPrefProvider: SharedPrefProvider,
newEntriesChannel: NewEntriesChannel,
debugChannel: DebugChannel,
appInfo: AppInfo appInfo: AppInfo
) { ) {
@ -37,8 +37,8 @@ class SyncManager @Inject constructor(
if (now().isHolidays) stopSyncWorker() if (now().isHolidays) stopSyncWorker()
if (SDK_INT > O) { if (SDK_INT > O) {
newEntriesChannel.create() channels.forEach { it.create() }
if (appInfo.isDebug) debugChannel.create() notificationManager.deleteNotificationChannel("new_entries_channel")
} }
if (sharedPrefProvider.getLong(APP_VERSION_CODE_KEY, -1L) != appInfo.versionCode.toLong()) { 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 android.content.Context
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.utils.AppInfo
import javax.inject.Inject import javax.inject.Inject
@TargetApi(26) @TargetApi(26)
class DebugChannel @Inject constructor( class DebugChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat, private val notificationManager: NotificationManagerCompat,
private val context: Context private val context: Context,
) { private val appInfo: AppInfo
) : Channel {
companion object { companion object {
const val CHANNEL_ID = "debug_channel" const val CHANNEL_ID = "debug_channel"
} }
fun create() { override fun create() {
if (appInfo.isDebug) return
notificationManager.createNotificationChannel( notificationManager.createNotificationChannel(
NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_debug), IMPORTANCE_DEFAULT) NotificationChannel(CHANNEL_ID, context.getString(R.string.channel_debug), IMPORTANCE_DEFAULT)
.apply { .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 package io.github.wulkanowy.services.sync.channels
import android.annotation.TargetApi import android.annotation.TargetApi
import android.app.Notification.VISIBILITY_PUBLIC import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager.IMPORTANCE_HIGH import android.app.NotificationManager
import android.content.Context import android.content.Context
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import io.github.wulkanowy.R import io.github.wulkanowy.R
import javax.inject.Inject import javax.inject.Inject
@TargetApi(26) @TargetApi(26)
class NewEntriesChannel @Inject constructor( class NewNotesChannel @Inject constructor(
private val notificationManager: NotificationManagerCompat, private val notificationManager: NotificationManagerCompat,
private val context: Context private val context: Context
) { ) : Channel {
companion object { companion object {
const val CHANNEL_ID = "new_entries_channel" const val CHANNEL_ID = "new_notes_channel"
} }
fun create() { override fun create() {
notificationManager.createNotificationChannel( 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 { .apply {
enableLights(true) enableLights(true)
enableVibration(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.db.entities.Student
import io.github.wulkanowy.data.repositories.grade.GradeRepository import io.github.wulkanowy.data.repositories.grade.GradeRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository 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.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.getCompatColor
@ -38,7 +38,7 @@ class GradeWork @Inject constructor(
} }
private fun notify(grades: List<Grade>) { 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)) .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)) .setContentText(context.resources.getQuantityString(R.plurals.grade_notify_new_items, grades.size, grades.size))
.setSmallIcon(R.drawable.ic_stat_grade) .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.db.entities.Student
import io.github.wulkanowy.data.repositories.luckynumber.LuckyNumberRepository import io.github.wulkanowy.data.repositories.luckynumber.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository 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.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.getCompatColor
@ -29,8 +29,8 @@ class LuckyNumberWork @Inject constructor(
) : Work { ) : Work {
override fun create(student: Student, semester: Semester): Completable { override fun create(student: Student, semester: Semester): Completable {
return luckyNumberRepository.getLuckyNumber(semester, true, preferencesRepository.isNotificationsEnable) return luckyNumberRepository.getLuckyNumber(student, true, preferencesRepository.isNotificationsEnable)
.flatMap { luckyNumberRepository.getNotNotifiedLuckyNumber(semester) } .flatMap { luckyNumberRepository.getNotNotifiedLuckyNumber(student) }
.flatMapCompletable { .flatMapCompletable {
notify(it) notify(it)
luckyNumberRepository.updateLuckyNumber(it.apply { isNotified = true }) luckyNumberRepository.updateLuckyNumber(it.apply { isNotified = true })
@ -38,7 +38,7 @@ class LuckyNumberWork @Inject constructor(
} }
private fun notify(luckyNumber: LuckyNumber) { 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)) .setContentTitle(context.getString(R.string.lucky_number_notify_new_item_title))
.setContentText(context.getString(R.string.lucky_number_notify_new_item, luckyNumber.luckyNumber)) .setContentText(context.getString(R.string.lucky_number_notify_new_item, luckyNumber.luckyNumber))
.setSmallIcon(R.drawable.ic_stat_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.MessageFolder.RECEIVED
import io.github.wulkanowy.data.repositories.message.MessageRepository import io.github.wulkanowy.data.repositories.message.MessageRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository 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.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.getCompatColor
@ -39,7 +39,7 @@ class MessageWork @Inject constructor(
} }
private fun notify(messages: List<Message>) { 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)) .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)) .setContentText(context.resources.getQuantityString(R.plurals.message_notify_new_items, messages.size, messages.size))
.setSmallIcon(R.drawable.ic_stat_message) .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.db.entities.Student
import io.github.wulkanowy.data.repositories.note.NoteRepository import io.github.wulkanowy.data.repositories.note.NoteRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository 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.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getCompatColor import io.github.wulkanowy.utils.getCompatColor
@ -38,7 +38,7 @@ class NoteWork @Inject constructor(
} }
private fun notify(notes: List<Note>) { 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)) .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)) .setContentText(context.resources.getQuantityString(R.plurals.note_notify_new_items, notes.size, notes.size))
.setSmallIcon(R.drawable.ic_stat_note) .setSmallIcon(R.drawable.ic_stat_note)

View File

@ -4,6 +4,7 @@ import android.content.Intent
import android.widget.RemoteViewsService import android.widget.RemoteViewsService
import dagger.android.AndroidInjection import dagger.android.AndroidInjection
import io.github.wulkanowy.data.db.SharedPrefProvider 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.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.data.repositories.timetable.TimetableRepository import io.github.wulkanowy.data.repositories.timetable.TimetableRepository
@ -22,6 +23,9 @@ class TimetableWidgetService : RemoteViewsService() {
@Inject @Inject
lateinit var semesterRepo: SemesterRepository lateinit var semesterRepo: SemesterRepository
@Inject
lateinit var prefRepository: PreferencesRepository
@Inject @Inject
lateinit var sharedPref: SharedPrefProvider lateinit var sharedPref: SharedPrefProvider
@ -30,6 +34,6 @@ class TimetableWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory { override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
AndroidInjection.inject(this) 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 eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseFragment 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.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.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
@ -45,7 +46,7 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
override val creatorsRes: Triple<String, String, Drawable?>? override val creatorsRes: Triple<String, String, Drawable?>?
get() = context?.run { 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?>? 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() { override fun openDiscordInvite() {
context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage) context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage)
} }
@ -150,7 +155,7 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
} }
override fun openCreators() { override fun openCreators() {
(activity as? MainActivity)?.pushView(CreatorFragment.newInstance()) (activity as? MainActivity)?.pushView(ContributorFragment.newInstance())
} }
override fun openPrivacyPolicy() { override fun openPrivacyPolicy() {

View File

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

View File

@ -25,6 +25,8 @@ interface AboutView : BaseView {
fun updateData(header: AboutScrollableHeader, items: List<AboutItem>) fun updateData(header: AboutScrollableHeader, items: List<AboutItem>)
fun openLogViewer()
fun openDiscordInvite() fun openDiscordInvite()
fun openEmailClient() 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.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -18,18 +18,18 @@ import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_creator.* import kotlinx.android.synthetic.main.fragment_creator.*
import javax.inject.Inject import javax.inject.Inject
class CreatorFragment : BaseFragment(), CreatorView, MainView.TitledView { class ContributorFragment : BaseFragment(), ContributorView, MainView.TitledView {
@Inject @Inject
lateinit var presenter: CreatorPresenter lateinit var presenter: ContributorPresenter
@Inject @Inject
lateinit var creatorsAdapter: FlexibleAdapter<AbstractFlexibleItem<*>> lateinit var creatorsAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
override val titleStringId get() = R.string.creators_title override val titleStringId get() = R.string.contributors_title
companion object { companion object {
fun newInstance() = CreatorFragment() fun newInstance() = ContributorFragment()
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -53,7 +53,7 @@ class CreatorFragment : BaseFragment(), CreatorView, MainView.TitledView {
creatorSeeMore.setOnClickListener { presenter.onSeeMoreClick() } creatorSeeMore.setOnClickListener { presenter.onSeeMoreClick() }
} }
override fun updateData(data: List<CreatorItem>) { override fun updateData(data: List<ContributorItem>) {
creatorsAdapter.updateDataSet(data) 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 android.view.View
import coil.api.load import coil.api.load
@ -10,11 +10,12 @@ import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.pojos.AppCreator import io.github.wulkanowy.data.pojos.AppCreator
import kotlinx.android.extensions.LayoutContainer 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) 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 (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
other as CreatorItem other as ContributorItem
if (creator != other.creator) return false 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 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.appcreator.AppCreatorRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.SchedulersProvider
import io.reactivex.Single
import javax.inject.Inject import javax.inject.Inject
class CreatorPresenter @Inject constructor( class ContributorPresenter @Inject constructor(
schedulers: SchedulersProvider, schedulers: SchedulersProvider,
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val appCreatorRepository: AppCreatorRepository 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) super.onAttachView(view)
view.initView() view.initView()
loadData() loadData()
} }
fun onItemSelected(item: AbstractFlexibleItem<*>) { fun onItemSelected(item: AbstractFlexibleItem<*>) {
if (item !is CreatorItem) return if (item !is ContributorItem) return
view?.openUserGithubPage(item.creator.githubUsername) view?.openUserGithubPage(item.creator.githubUsername)
} }
@ -34,7 +32,7 @@ class CreatorPresenter @Inject constructor(
private fun loadData() { private fun loadData() {
disposable.add(appCreatorRepository.getAppCreators() disposable.add(appCreatorRepository.getAppCreators()
.map { it.map { creator -> CreatorItem(creator) } } .map { it.map { creator -> ContributorItem(creator) } }
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doFinally { view?.showProgress(false) } .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 import io.github.wulkanowy.ui.base.BaseView
interface CreatorView : BaseView { interface ContributorView : BaseView {
fun initView() fun initView()
fun updateData(data: List<CreatorItem>) fun updateData(data: List<ContributorItem>)
fun openUserGithubPage(username: String) 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() fun newInstance() = AttendanceSummaryFragment()
} }
override val totalString get() = getString(R.string.attendance_summary_total)
override val titleStringId get() = R.string.attendance_title override val titleStringId get() = R.string.attendance_title
override val isViewEmpty get() = attendanceSummaryAdapter.isEmpty 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.SchedulersProvider
import io.github.wulkanowy.utils.calculatePercentage import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.getFormattedName import io.github.wulkanowy.utils.getFormattedName
import org.threeten.bp.Month
import timber.log.Timber import timber.log.Timber
import java.lang.String.format import java.lang.String.format
import java.util.Locale.FRANCE 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> { 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( AttendanceSummaryItem(
month = it.month.getFormattedName(), month = it.month.getFormattedName(),
percentage = formatPercentage(it.calculatePercentage()), percentage = formatPercentage(it.calculatePercentage()),

View File

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

View File

@ -40,6 +40,8 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie
override val titleStringId get() = R.string.grade_title override val titleStringId get() = R.string.grade_title
override var subtitleString = ""
override val currentPageIndex get() = gradeViewPager.currentItem override val currentPageIndex get() = gradeViewPager.currentItem
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -133,6 +135,11 @@ class GradeFragment : BaseFragment(), GradeView, MainView.MainChildView, MainVie
.show() .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() { fun onChildRefresh() {
presenter.onChildViewRefresh() 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 import io.github.wulkanowy.ui.modules.grade.summary.GradeSummaryFragment
@Suppress("unused") @Suppress("unused")
@Module(includes = [GradeModule.Static::class]) @Module
abstract class GradeModule { abstract class GradeModule {
@Module companion object {
object Static {
@PerFragment @PerFragment
@Provides @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.ui.base.ErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.getCurrentOrLast
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject import javax.inject.Inject
class GradePresenter @Inject constructor( class GradePresenter @Inject constructor(
@ -21,6 +23,8 @@ class GradePresenter @Inject constructor(
var selectedIndex = 0 var selectedIndex = 0
private set private set
private var schoolYear = 0
private var semesters = emptyList<Semester>() private var semesters = emptyList<Semester>()
private val loadedSemesterId = mutableMapOf<Int, Int>() private val loadedSemesterId = mutableMapOf<Int, Int>()
@ -56,6 +60,7 @@ class GradePresenter @Inject constructor(
selectedIndex = index + 1 selectedIndex = index + 1
loadedSemesterId.clear() loadedSemesterId.clear()
view?.let { view?.let {
it.setCurrentSemesterName(index + 1, schoolYear)
notifyChildrenSemesterChange() notifyChildrenSemesterChange()
loadChild(it.currentPageIndex) loadChild(it.currentPageIndex)
} }
@ -95,17 +100,17 @@ class GradePresenter @Inject constructor(
private fun loadData() { private fun loadData() {
Timber.i("Loading grade data started") Timber.i("Loading grade data started")
disposable.add(studentRepository.getCurrentStudent() disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getSemesters(it) } .flatMap { semesterRepository.getSemesters(it, refreshOnNoCurrent = true) }
.doOnSuccess { .delay(200, MILLISECONDS)
it.first { item -> item.isCurrent }.also { current ->
selectedIndex = if (selectedIndex == 0) current.semesterName else selectedIndex
semesters = it.filter { semester -> semester.diaryId == current.diaryId }
}
}
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doFinally { view?.showProgress(false) }
.subscribe({ .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 { view?.run {
Timber.i("Loading grade result: Attempt load index $currentPageIndex") Timber.i("Loading grade result: Attempt load index $currentPageIndex")
loadChild(currentPageIndex) loadChild(currentPageIndex)
@ -121,6 +126,7 @@ class GradePresenter @Inject constructor(
private fun showErrorViewOnError(message: String, error: Throwable) { private fun showErrorViewOnError(message: String, error: Throwable) {
lastError = error lastError = error
view?.run { view?.run {
showProgress(false)
showErrorView(true) showErrorView(true)
setErrorDetails(message) setErrorDetails(message)
} }

View File

@ -20,6 +20,8 @@ interface GradeView : BaseView {
fun showSemesterDialog(selectedIndex: Int) fun showSemesterDialog(selectedIndex: Int)
fun setCurrentSemesterName(semester: Int, schoolYear: Int)
fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean) fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean)
fun notifyChildParentReselected(index: Int) 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 package io.github.wulkanowy.ui.modules.grade.statistics
import android.graphics.Color
import android.graphics.Color.WHITE
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.recyclerview.widget.LinearLayoutManager
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.R
import io.github.wulkanowy.data.db.entities.GradePointsStatistics import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.setOnItemSelectedListener import io.github.wulkanowy.utils.setOnItemSelectedListener
import kotlinx.android.synthetic.main.fragment_grade_statistics.* import kotlinx.android.synthetic.main.fragment_grade_statistics.*
import javax.inject.Inject import javax.inject.Inject
@ -35,6 +22,9 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
@Inject @Inject
lateinit var presenter: GradeStatisticsPresenter lateinit var presenter: GradeStatisticsPresenter
@Inject
lateinit var statisticsAdapter: GradeStatisticsAdapter
private lateinit var subjectsAdapter: ArrayAdapter<String> private lateinit var subjectsAdapter: ArrayAdapter<String>
companion object { companion object {
@ -43,9 +33,7 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
fun newInstance() = GradeStatisticsFragment() fun newInstance() = GradeStatisticsFragment()
} }
override val isPieViewEmpty get() = gradeStatisticsChart.isEmpty override val isViewEmpty get() = statisticsAdapter.items.isEmpty()
override val isBarViewEmpty get() = gradeStatisticsChartPoints.isEmpty
override val currentType override val currentType
get() = when (gradeStatisticsTypeSwitch.checkedRadioButtonId) { get() = when (gradeStatisticsTypeSwitch.checkedRadioButtonId) {
@ -54,35 +42,6 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
else -> ViewType.POINTS 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? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_grade_statistics, container, false) return inflater.inflate(R.layout.fragment_grade_statistics, container, false)
} }
@ -94,31 +53,9 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
} }
override fun initView() { override fun initView() {
with(gradeStatisticsChart) { with(gradeStatisticsRecycler) {
description.isEnabled = false layoutManager = LinearLayoutManager(requireContext())
setHoleColor(context.getThemeAttrColor(android.R.attr.windowBackground)) adapter = statisticsAdapter
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
}
} }
subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf()) 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) { override fun updateData(items: List<GradeStatisticsItem>, theme: String) {
gradeColors = when (theme) { statisticsAdapter.theme = theme
"vulcan" -> vulcanGradeColors statisticsAdapter.items = items
else -> materialGradeColors statisticsAdapter.notifyDataSetChanged()
}
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 showSubjects(show: Boolean) { override fun showSubjects(show: Boolean) {
@ -232,16 +93,15 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
} }
override fun clearView() { override fun clearView() {
gradeStatisticsChart.clear() statisticsAdapter.items = emptyList()
gradeStatisticsChartPoints.clear()
} }
override fun showPieContent(show: Boolean) { override fun resetView() {
gradeStatisticsChart.visibility = if (show) View.VISIBLE else View.GONE gradeStatisticsScroll.scrollTo(0, 0)
} }
override fun showBarContent(show: Boolean) { override fun showContent(show: Boolean) {
gradeStatisticsChartPoints.visibility = if (show) View.VISIBLE else View.GONE gradeStatisticsRecycler.visibility = if (show) View.VISIBLE else View.GONE
} }
override fun showEmpty(show: Boolean) { override fun showEmpty(show: Boolean) {
@ -273,7 +133,7 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
} }
override fun onParentReselected() { override fun onParentReselected() {
// presenter.onParentViewReselected()
} }
override fun onParentChangeSemester() { override fun onParentChangeSemester() {

View File

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

View File

@ -1,14 +1,11 @@
package io.github.wulkanowy.ui.modules.grade.statistics package io.github.wulkanowy.ui.modules.grade.statistics
import io.github.wulkanowy.data.db.entities.GradePointsStatistics import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
interface GradeStatisticsView : BaseView { interface GradeStatisticsView : BaseView {
val isPieViewEmpty: Boolean val isViewEmpty: Boolean
val isBarViewEmpty: Boolean
val currentType: ViewType val currentType: ViewType
@ -16,9 +13,7 @@ interface GradeStatisticsView : BaseView {
fun updateSubjects(data: ArrayList<String>) fun updateSubjects(data: ArrayList<String>)
fun updatePieData(items: List<GradeStatistics>, theme: String) fun updateData(items: List<GradeStatisticsItem>, theme: String)
fun updateBarData(item: GradePointsStatistics)
fun showSubjects(show: Boolean) fun showSubjects(show: Boolean)
@ -28,9 +23,9 @@ interface GradeStatisticsView : BaseView {
fun clearView() fun clearView()
fun showPieContent(show: Boolean) fun resetView()
fun showBarContent(show: Boolean) fun showContent(show: Boolean)
fun showEmpty(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.base.BaseFragmentPagerAdapter
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment 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.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.studentselect.LoginStudentSelectFragment
import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment
import io.github.wulkanowy.utils.setOnSelectPageListener import io.github.wulkanowy.utils.setOnSelectPageListener
@ -52,7 +53,8 @@ class LoginActivity : BaseActivity<LoginPresenter>(), LoginView {
LoginFormFragment.newInstance(), LoginFormFragment.newInstance(),
LoginSymbolFragment.newInstance(), LoginSymbolFragment.newInstance(),
LoginStudentSelectFragment.newInstance(), LoginStudentSelectFragment.newInstance(),
LoginAdvancedFragment.newInstance() LoginAdvancedFragment.newInstance(),
LoginRecoverFragment.newInstance()
)) ))
} }
@ -99,4 +101,8 @@ class LoginActivity : BaseActivity<LoginPresenter>(), LoginView {
fun onAdvancedLoginClick() { fun onAdvancedLoginClick() {
presenter.onAdvancedLoginClick() presenter.onAdvancedLoginClick()
} }
fun onRecoverClick() {
presenter.onRecoverClick()
}
} }

View File

@ -43,5 +43,8 @@ class LoginErrorHandler @Inject constructor(
super.clear() super.clear()
onBadCredentials = {} onBadCredentials = {}
onStudentDuplicate = {} 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.base.BaseFragmentPagerAdapter
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment 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.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.studentselect.LoginStudentSelectFragment
import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment import io.github.wulkanowy.ui.modules.login.symbol.LoginSymbolFragment
@Suppress("unused") @Suppress("unused")
@Module(includes = [LoginModule.Static::class]) @Module
internal abstract class LoginModule { internal abstract class LoginModule {
@Module companion object {
object Static {
@PerActivity @PerActivity
@Provides @Provides
@ -38,4 +38,8 @@ internal abstract class LoginModule {
@PerFragment @PerFragment
@ContributesAndroidInjector @ContributesAndroidInjector
abstract fun bindLoginSelectStudentFragment(): LoginStudentSelectFragment abstract fun bindLoginSelectStudentFragment(): LoginStudentSelectFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindLoginRecoverFragment(): LoginRecoverFragment
} }

View File

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

View File

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

View File

@ -5,11 +5,13 @@ import io.github.wulkanowy.ui.base.BaseView
interface LoginAdvancedView : BaseView { interface LoginAdvancedView : BaseView {
val formNameValue: String val formUsernameValue: String
val formPassValue: String val formPassValue: String
val formHostValue: String? val formHostValue: String
val formHostSymbol: String
val formLoginType: String val formLoginType: String
@ -19,11 +21,25 @@ interface LoginAdvancedView : BaseView {
val formTokenValue: String val formTokenValue: String
val nicknameLabel: String
val emailLabel: String
fun initView() 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) fun setErrorPassRequired(focus: Boolean)
@ -31,7 +47,7 @@ interface LoginAdvancedView : BaseView {
fun setErrorPassIncorrect() fun setErrorPassIncorrect()
fun clearNameError() fun clearUsernameError()
fun clearPassError() fun clearPassError()

View File

@ -7,8 +7,7 @@ import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE import android.widget.ArrayAdapter
import android.view.inputmethod.EditorInfo.IME_NULL
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student 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.hideSoftInput
import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.setOnEditorDoneSignIn
import io.github.wulkanowy.utils.showSoftInput import io.github.wulkanowy.utils.showSoftInput
import kotlinx.android.synthetic.main.fragment_login_form.* import kotlinx.android.synthetic.main.fragment_login_form.*
import javax.inject.Inject import javax.inject.Inject
@ -34,16 +34,33 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
fun newInstance() = LoginFormFragment() 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 hostKeys: Array<String>
private lateinit var hostValues: Array<String> private lateinit var hostValues: Array<String>
private lateinit var hostSymbols: Array<String>
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_login_form, container, false) return inflater.inflate(R.layout.fragment_login_form, container, false)
} }
@ -56,38 +73,61 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
override fun initView() { override fun initView() {
hostKeys = resources.getStringArray(R.array.hosts_keys) hostKeys = resources.getStringArray(R.array.hosts_keys)
hostValues = resources.getStringArray(R.array.hosts_values) 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() } loginFormPass.doOnTextChanged { _, _, _, _ -> presenter.onPassTextChanged() }
loginFormSymbol.doOnTextChanged { _, _, _, _ -> presenter.onSymbolTextChanged() }
loginFormHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() } loginFormHost.setOnItemClickListener { _, _, _, _ -> presenter.onHostSelected() }
loginFormSignIn.setOnClickListener { presenter.onSignInClick() } loginFormSignIn.setOnClickListener { presenter.onSignInClick() }
loginFormAdvancedButton.setOnClickListener { presenter.onAdvancedLoginClick() } loginFormAdvancedButton.setOnClickListener { presenter.onAdvancedLoginClick() }
loginFormPrivacyLink.setOnClickListener { presenter.onPrivacyLinkClick() } loginFormPrivacyLink.setOnClickListener { presenter.onPrivacyLinkClick() }
loginFormFaq.setOnClickListener { presenter.onFaqClick() } loginFormFaq.setOnClickListener { presenter.onFaqClick() }
loginFormContactEmail.setOnClickListener { presenter.onEmailClick() } loginFormContactEmail.setOnClickListener { presenter.onEmailClick() }
loginFormRecoverLink.setOnClickListener { presenter.onRecoverClick() }
loginFormPass.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() }
loginFormSymbol.setOnEditorDoneSignIn { loginFormSignIn.callOnClick() }
loginFormPass.setOnEditorActionListener { _, id, _ -> loginFormSymbol.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, resources.getStringArray(R.array.symbols_values)))
if (id == IME_ACTION_DONE || id == IME_NULL) loginFormSignIn.callOnClick() else false
}
with(loginFormHost) { with(loginFormHost) {
setText(hostKeys.getOrElse(0) { "" }) setText(hostKeys.getOrNull(0).orEmpty())
setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys)) setAdapter(LoginSymbolAdapter(context, R.layout.support_simple_spinner_dropdown_item, hostKeys))
setOnClickListener { if (loginFormContainer.visibility == GONE) dismissDropDown() }
} }
} }
override fun setCredentials(name: String, pass: String) { override fun setCredentials(username: String, pass: String) {
loginFormName.setText(name) loginFormUsername.setText(username)
loginFormPass.setText(pass) loginFormPass.setText(pass)
} }
override fun setErrorNameRequired() { override fun setSymbol(symbol: String) {
with(loginFormNameLayout) { 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() requestFocus()
error = getString(R.string.login_field_required) 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) { override fun setErrorPassRequired(focus: Boolean) {
with(loginFormPassLayout) { with(loginFormPassLayout) {
if (focus) requestFocus() if (focus) requestFocus()
@ -109,14 +149,18 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
} }
} }
override fun clearNameError() { override fun clearUsernameError() {
loginFormNameLayout.error = null loginFormUsernameLayout.error = null
} }
override fun clearPassError() { override fun clearPassError() {
loginFormPassLayout.error = null loginFormPassLayout.error = null
} }
override fun clearSymbolError() {
loginFormSymbolLayout.error = null
}
override fun showSoftKeyboard() { override fun showSoftKeyboard() {
activity?.showSoftInput() activity?.showSoftInput()
} }
@ -154,6 +198,10 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
(activity as? LoginActivity)?.onAdvancedLoginClick() (activity as? LoginActivity)?.onAdvancedLoginClick()
} }
override fun onRecoverClick() {
(activity as? LoginActivity)?.onRecoverClick()
}
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
presenter.onDetachView() 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) 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() { override fun openEmail() {
context?.openEmailClient( context?.openEmailClient(
requireContext().getString(R.string.login_email_intent_title), requireContext().getString(R.string.login_email_intent_title),

View File

@ -42,10 +42,25 @@ class LoginFormPresenter @Inject constructor(
fun onHostSelected() { fun onHostSelected() {
view?.apply { view?.apply {
clearPassError() clearPassError()
clearNameError() clearUsernameError()
if (formHostValue?.contains("fakelog") == true) { if (formHostValue.contains("fakelog")) {
setCredentials("jan@fakelog.cf", "jan123") 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() view?.clearPassError()
} }
fun onNameTextChanged() { fun onUsernameTextChanged() {
view?.clearNameError() view?.clearUsernameError()
}
fun onSymbolTextChanged() {
view?.clearSymbolError()
} }
fun onSignInClick() { fun onSignInClick() {
val email = view?.formNameValue.orEmpty().trim() val email = view?.formUsernameValue.orEmpty().trim()
val password = view?.formPassValue.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) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doOnSubscribe { .doOnSubscribe {
@ -83,11 +103,11 @@ class LoginFormPresenter @Inject constructor(
} }
.subscribe({ .subscribe({
Timber.i("Login result: Success") Timber.i("Login result: Success")
analytics.logEvent("registration_form", "success" to true, "students" to it.size, "scrapperBaseUrl" to endpoint, "error" to "No error") analytics.logEvent("registration_form", "success" to true, "students" to it.size, "scrapperBaseUrl" to host, "error" to "No error")
view?.notifyParentAccountLogged(it, Triple(email, password, endpoint)) view?.notifyParentAccountLogged(it, Triple(email, password, host))
}, { }, {
Timber.i("Login result: An exception occurred") 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) loginErrorHandler.dispatch(it)
view?.showContact(true) view?.showContact(true)
})) }))
@ -101,11 +121,15 @@ class LoginFormPresenter @Inject constructor(
view?.openEmail() 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 var isCorrect = true
if (login.isEmpty()) { if (login.isEmpty()) {
view?.setErrorNameRequired() view?.setErrorUsernameRequired()
isCorrect = false isCorrect = false
} }
@ -118,6 +142,12 @@ class LoginFormPresenter @Inject constructor(
view?.setErrorPassInvalid(focus = isCorrect) view?.setErrorPassInvalid(focus = isCorrect)
isCorrect = false isCorrect = false
} }
if ("standard" !in host && symbol.isBlank()) {
view?.setErrorSymbolRequired(focus = isCorrect)
isCorrect = false
}
return isCorrect return isCorrect
} }
} }

View File

@ -7,15 +7,31 @@ interface LoginFormView : BaseView {
fun initView() fun initView()
val formNameValue: String val formUsernameValue: String
val formPassValue: 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) fun setErrorPassRequired(focus: Boolean)
@ -23,10 +39,12 @@ interface LoginFormView : BaseView {
fun setErrorPassIncorrect() fun setErrorPassIncorrect()
fun clearNameError() fun clearUsernameError()
fun clearPassError() fun clearPassError()
fun clearSymbolError()
fun showSoftKeyboard() fun showSoftKeyboard()
fun hideSoftKeyboard() fun hideSoftKeyboard()
@ -48,4 +66,6 @@ interface LoginFormView : BaseView {
fun openEmail() fun openEmail()
fun openAdvancedLogin() 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.annotation.SuppressLint
import android.view.View import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible 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.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_login_student_select.* 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 override fun getLayoutRes() = R.layout.item_login_student_select
@ -24,6 +27,10 @@ class LoginStudentSelectItem(val student: Student) : AbstractFlexibleItem<LoginS
holder.apply { holder.apply {
loginItemName.text = "${student.studentName} ${student.className}" loginItemName.text = "${student.studentName} ${student.className}"
loginItemSchool.text = student.schoolName 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 other as LoginStudentSelectItem
if (student != other.student) return false if (student != other.student) return false
if (alreadySaved != other.alreadySaved) return false
return true return true
} }
@ -42,7 +50,8 @@ class LoginStudentSelectItem(val student: Student) : AbstractFlexibleItem<LoginS
return student.hashCode() 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 override val containerView: View
get() = itemView get() = itemView
@ -53,7 +62,10 @@ class LoginStudentSelectItem(val student: Student) : AbstractFlexibleItem<LoginS
override fun onClick(view: View?) { override fun onClick(view: View?) {
super.onClick(view) super.onClick(view)
if (loginItemCheck.isEnabled) {
loginItemCheck.apply { isChecked = !isChecked } 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.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.ifNullOrBlank import io.github.wulkanowy.utils.ifNullOrBlank
import io.reactivex.Single
import timber.log.Timber import timber.log.Timber
import java.io.Serializable import java.io.Serializable
import javax.inject.Inject import javax.inject.Inject
@ -50,7 +51,7 @@ class LoginStudentSelectPresenter @Inject constructor(
} }
fun onItemSelected(item: AbstractFlexibleItem<*>?) { fun onItemSelected(item: AbstractFlexibleItem<*>?) {
if (item is LoginStudentSelectItem) { if (item is LoginStudentSelectItem && !item.alreadySaved) {
selectedStudents.removeAll { it == item.student } selectedStudents.removeAll { it == item.student }
.let { if (!it) selectedStudents.add(item.student) } .let { if (!it) selectedStudents.add(item.student) }
@ -58,12 +59,34 @@ 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>) { private fun loadData(students: List<Student>) {
this.students = students this.students = students
view?.apply { disposable.add(studentRepository.getSavedStudents()
updateData(students.map { LoginStudentSelectItem(it) }) .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>) { private fun registerStudents(students: List<Student>) {
disposable.add(studentRepository.saveStudents(students) disposable.add(studentRepository.saveStudents(students)

View File

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

View File

@ -1,21 +1,14 @@
package io.github.wulkanowy.ui.modules.luckynumberwidget package io.github.wulkanowy.ui.modules.luckynumberwidget
import android.annotation.TargetApi
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.appwidget.AppWidgetManager 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_HEIGHT
import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH
import android.content.BroadcastReceiver import android.appwidget.AppWidgetProvider
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Bundle
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.widget.RemoteViews 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.db.entities.LuckyNumber
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.repositories.luckynumber.LuckyNumberRepository 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.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
@ -34,14 +26,11 @@ import io.reactivex.Maybe
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class LuckyNumberWidgetProvider : BroadcastReceiver() { class LuckyNumberWidgetProvider : AppWidgetProvider() {
@Inject @Inject
lateinit var studentRepository: StudentRepository lateinit var studentRepository: StudentRepository
@Inject
lateinit var semesterRepository: SemesterRepository
@Inject @Inject
lateinit var luckyNumberRepository: LuckyNumberRepository lateinit var luckyNumberRepository: LuckyNumberRepository
@ -59,20 +48,20 @@ class LuckyNumberWidgetProvider : BroadcastReceiver() {
fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId" fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId"
fun getThemeWidgetKey(appWidgetId: Int) = "lucky_number_widget_theme_$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) { override fun onReceive(context: Context, intent: Intent) {
AndroidInjection.inject(this, context) AndroidInjection.inject(this, context)
when (intent.action) { super.onReceive(context, intent)
ACTION_APPWIDGET_UPDATE -> onUpdate(context, intent)
ACTION_APPWIDGET_DELETED -> onDelete(intent)
ACTION_APPWIDGET_OPTIONS_CHANGED -> onOptionsChange(context, intent)
}
} }
private fun onUpdate(context: Context, intent: Intent) { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray?) {
intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS)?.forEach { appWidgetId -> super.onUpdate(context, appWidgetManager, appWidgetIds)
appWidgetIds?.forEach { appWidgetId ->
val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0) val savedTheme = sharedPref.getLong(getThemeWidgetKey(appWidgetId), 0)
val layoutId = if (savedTheme == 0L) R.layout.widget_luckynumber else R.layout.widget_luckynumber_dark val layoutId = if (savedTheme == 0L) R.layout.widget_luckynumber else R.layout.widget_luckynumber_dark
@ -85,15 +74,65 @@ class LuckyNumberWidgetProvider : BroadcastReceiver() {
setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent) setOnClickPendingIntent(R.id.luckyNumberWidgetContainer, appIntent)
} }
setStyles(remoteView, intent) setStyles(remoteView, appWidgetId)
appWidgetManager.updateAppWidget(appWidgetId, remoteView) appWidgetManager.updateAppWidget(appWidgetId, remoteView)
} }
} }
private fun onDelete(intent: Intent) { override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
val appWidgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID, 0) super.onDeleted(context, appWidgetIds)
appWidgetIds?.forEach { appWidgetId ->
if (appWidgetId != 0) sharedPref.delete(getStudentWidgetKey(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? { private fun getLuckyNumber(studentId: Long, appWidgetId: Int): LuckyNumber? {
return try { return try {
@ -113,7 +152,6 @@ class LuckyNumberWidgetProvider : BroadcastReceiver() {
else -> Maybe.empty() else -> Maybe.empty()
} }
} }
.flatMap { semesterRepository.getCurrentSemester(it).toMaybe() }
.flatMap { luckyNumberRepository.getLuckyNumber(it) } .flatMap { luckyNumberRepository.getLuckyNumber(it) }
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.blockingGet() .blockingGet()
@ -124,69 +162,4 @@ class LuckyNumberWidgetProvider : BroadcastReceiver() {
null 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 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 startMenuIndex = 0
override var startMenuMoreIndex = -1 override var startMenuMoreIndex = -1
@ -152,6 +154,10 @@ class MainActivity : BaseActivity<MainPresenter>(), MainView {
supportActionBar?.title = title supportActionBar?.title = title
} }
override fun setViewSubTitle(subtitle: String?) {
supportActionBar?.subtitle = subtitle
}
override fun showHomeArrow(show: Boolean) { override fun showHomeArrow(show: Boolean) {
supportActionBar?.setDisplayHomeAsUpEnabled(show) supportActionBar?.setDisplayHomeAsUpEnabled(show)
} }

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