Compare commits

...

35 Commits
0.7.4 ... 0.8.2

Author SHA1 Message Date
80cb94c434 Version 0.8.2 2019-05-15 19:26:25 +02:00
0cb4eda32b Fix crash on reselecting fragment (#339) 2019-05-15 15:11:28 +02:00
d169f964f2 Fix crash when no webview activity (#338) 2019-05-14 11:45:27 +02:00
103ab95c80 Fix not attached fragment (#337)
* Add condition for error dialog
* Add safe call of forEach in LuckyNumberWidgetProvider
2019-05-09 22:06:11 +02:00
a191f03cdf Version 0.8.1 2019-04-30 23:56:22 +02:00
63404b8576 Fix entity list comparing (#335) 2019-04-30 19:04:05 +02:00
1b7760ff88 Fix dark theme background with custom theme engine (#334) 2019-04-30 17:45:37 +02:00
24f58835e7 Fix menu view initialization and restoration (#333) 2019-04-30 17:28:09 +02:00
b032c459d1 Fix theme on release build (#332) 2019-04-30 11:16:23 +02:00
df0a1e59cc Version 0.8.0 2019-04-29 20:39:35 +02:00
dbbc8069b1 Add showing proper fragment in notifications (#331) 2019-04-29 19:56:23 +02:00
f84040109c Add lucky number widget (#292) 2019-04-29 14:33:33 +02:00
baf1420193 Improve login student selection layout (#329) 2019-04-29 00:55:30 +02:00
4a36d78709 Add activity and fragment lifecycle logging (#327) 2019-04-26 23:53:02 +02:00
4464812651 Fix configure activity's theme (#325) 2019-04-22 09:27:25 +02:00
72a35481e5 Fix showing last message after update (#326) 2019-04-22 09:13:26 +02:00
017c200115 Fix grade summary final grade string (#324) 2019-04-20 22:19:06 +02:00
2bf7755157 Add AMOLED mode (#279) 2019-04-19 23:52:34 +02:00
269af4b7ba Update dependencies (#323) 2019-04-18 16:38:49 +02:00
7431738366 Add a selection of multiple students to login (#318) 2019-04-18 12:18:58 +02:00
034b99c7ab Add counting of the full-year average to the summary of grades (#322) 2019-04-18 00:32:43 +02:00
74e98e4430 Change style of privacy policy link (#321) 2019-04-09 23:33:53 +02:00
cbf3215dd1 Merge branch '0.7.x' 2019-04-08 13:51:02 +02:00
c18877466f Add account picker for timetable widget (#314)
Close #281
2019-04-08 00:18:45 +02:00
6cd6cae1e0 Version 0.7.6 2019-04-07 11:08:46 +02:00
333f7bfa16 Add privacy policy link (#320) 2019-04-07 10:59:27 +02:00
aa6dcaff94 Merge branch '0.7.x' 2019-04-06 01:18:03 +02:00
f2fa04105d Version 0.7.5 2019-04-06 01:06:11 +02:00
7d97d71066 Fix message loading if student and parent are logged in (#319)
Fixes #316
2019-04-05 19:35:21 +02:00
8daea5c900 Add class name to student (#315) 2019-04-04 13:00:07 +02:00
2bff468e56 Add deleting messages (#290) 2019-03-31 22:01:04 +02:00
f2855d598d Merge branch '0.7.x' 2019-03-30 20:19:34 +01:00
1ebc296bfe Merge branch '0.7.x' 2019-03-26 21:22:17 +01:00
a2a18e5652 Merge branch '0.7.x' 2019-03-24 23:22:44 +01:00
cd1ceea860 Add messages forwarding (#288) 2019-03-21 22:55:47 +01:00
172 changed files with 3938 additions and 670 deletions

View File

@ -7,11 +7,11 @@ references:
container_config: &container_config
docker:
- image: circleci/android:api-28-alpha
- image: circleci/android:api-28
working_directory: *workspace_root
environment:
environment:
JVM_OPTS: -Xmx3200m
_JAVA_OPTS: -Xmx3072m
attach_workspace: &attach_workspace
attach_workspace:

View File

@ -21,31 +21,6 @@
<option name="CONTINUATION_INDENT_IN_ELVIS" value="false" />
<option name="WRAP_ELVIS_EXPRESSIONS" value="0" />
</JetCodeStyleSettings>
<Objective-C-extensions>
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" fileNamingConvention="NONE" />
<pair source="c" header="h" fileNamingConvention="NONE" />
</extensions>
</Objective-C-extensions>
<XML>
<option name="XML_KEEP_LINE_BREAKS" value="false" />
<option name="XML_ALIGN_ATTRIBUTES" value="false" />

View File

@ -13,8 +13,8 @@ cache:
#branches:
# only:
# - master
# - 0.7.x
# - master
# - 0.8.x
android:
licenses:

View File

@ -16,13 +16,13 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 15
targetSdkVersion 28
versionCode 30
versionName "0.7.4"
versionCode 35
versionName "0.8.2"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
manifestPlaceholders = [
fabric_api_key: System.getenv("FABRIC_API_KEY") ?: "null",
fabric_api_key : System.getenv("FABRIC_API_KEY") ?: "null",
crashlytics_enabled: project.hasProperty("enableCrashlytics")
]
javaCompileOptions {
@ -80,53 +80,54 @@ androidExtensions {
play {
serviceAccountEmail = System.getenv("PLAY_SERVICE_ACCOUNT_EMAIL") ?: "jan@fakelog.cf"
serviceAccountCredentials = file('key.p12')
defaultToAppBundles = true
defaultToAppBundles = false
track = 'alpha'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation('io.github.wulkanowy:api:0.7.4') { exclude module: "threetenbp" }
implementation 'io.github.wulkanowy:api:0.8.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.legacy:legacy-support-v4:1.0.0"
implementation "androidx.appcompat:appcompat:1.0.2"
implementation "androidx.cardview:cardview:1.0.0"
implementation "com.google.android.material:material:1.0.0"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.multidex:multidex:2.0.1'
implementation "android.arch.work:work-runtime:1.0.0"
implementation "android.arch.work:work-rxjava2:1.0.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation "com.google.android.material:material:1.1.0-alpha05"
implementation 'com.github.wulkanowy:MaterialChipsInput:b72fd0ee6f'
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
implementation "androidx.room:room-runtime:2.1.0-alpha06"
implementation "androidx.room:room-rxjava2:2.1.0-alpha06"
kapt "androidx.room:room-compiler:2.1.0-alpha06"
implementation "androidx.work:work-runtime:2.0.1"
implementation "androidx.work:work-rxjava2:2.0.1"
implementation 'com.takisoft.preferencex:preferencex:1.0.0'
implementation "androidx.room:room-runtime:2.1.0-alpha07"
implementation "androidx.room:room-rxjava2:2.1.0-alpha07"
kapt "androidx.room:room-compiler:2.1.0-alpha07"
implementation 'com.squareup.inject:assisted-inject-annotations-dagger2:0.3.3'
kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.3.3'
implementation "com.google.dagger:dagger-android-support:2.21"
kapt "com.google.dagger:dagger-compiler:2.21"
kapt "com.google.dagger:dagger-android-processor:2.21"
implementation "com.google.dagger:dagger-android-support:2.22.1"
kapt "com.google.dagger:dagger-compiler:2.22.1"
kapt "com.google.dagger:dagger-android-processor:2.22.1"
implementation 'com.squareup.inject:assisted-inject-annotations-dagger2:0.4.0'
kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.4.0'
implementation "eu.davidea:flexible-adapter:5.1.0"
implementation "eu.davidea:flexible-adapter-ui:1.0.0"
implementation "com.aurelhubert:ahbottomnavigation:2.3.4"
implementation 'com.ncapdevi:frag-nav:3.2.0'
implementation 'com.github.wulkanowy:MaterialChipsInput:b72fd0ee6f'
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation "io.reactivex.rxjava2:rxjava:2.2.7"
implementation "io.reactivex.rxjava2:rxjava:2.2.8"
implementation 'com.google.code.gson:gson:2.8.5'
implementation "com.jakewharton.threetenabp:threetenabp:1.2.0"
implementation "com.jakewharton.timber:timber:4.7.1"
implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation "com.squareup.okhttp3:logging-interceptor:3.12.1"
implementation "com.mikepenz:aboutlibraries:6.2.3"
implementation 'com.takisoft.preferencex:preferencex:1.0.0'
implementation 'com.google.firebase:firebase-core:16.0.8'
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.9'
@ -138,15 +139,15 @@ dependencies {
testImplementation "junit:junit:4.12"
testImplementation "io.mockk:mockk:1.9.2"
testImplementation "org.mockito:mockito-inline:2.25.1"
testImplementation "org.mockito:mockito-inline:2.27.0"
testImplementation 'org.threeten:threetenbp:1.3.8'
androidTestImplementation 'androidx.test:core:1.1.0'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation "io.mockk:mockk-android:1.9.2"
androidTestImplementation 'org.mockito:mockito-android:2.25.1'
androidTestImplementation "androidx.room:room-testing:2.1.0-alpha06"
androidTestImplementation 'org.mockito:mockito-android:2.27.0'
androidTestImplementation "androidx.room:room-testing:2.1.0-alpha07"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
}

View File

@ -31,17 +31,17 @@ task jacocoTestReport(type: JacocoReport) {
'**/*_Provide*Factory*.*',
'**/*_Factory.*']
classDirectories = fileTree(
classDirectories.setFrom(fileTree(
dir: "$buildDir/intermediates/classes/debug",
excludes: excludes
) + fileTree(
dir: "$buildDir/tmp/kotlin-classes/debug",
excludes: excludes
)
))
sourceDirectories = files("$project.projectDir/src/main/java")
executionData = fileTree(
sourceDirectories.setFrom(files("$project.projectDir/src/main/java"))
executionData.setFrom(fileTree(
dir: project.projectDir,
includes: ["**/*.exec", "**/*.ec"]
)
))
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry
import io.github.wulkanowy.data.db.AppDatabase
import org.junit.Rule
abstract class AbstractMigrationTest {
val dbName = "migration-test"
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
fun getMigratedRoomDatabase(): AppDatabase {
val database = Room.databaseBuilder(ApplicationProvider.getApplicationContext(),
AppDatabase::class.java, dbName)
.addMigrations(Migration12(), Migration13())
.build()
// close the database and release any stream resources when the test finishes
helper.closeWhenFinished(database)
return database
}
}

View File

@ -2,33 +2,17 @@ package io.github.wulkanowy.data.db.migrations
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL
import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import io.github.wulkanowy.data.db.AppDatabase
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class)
class Migration12Test {
private val dbName = "migration-test"
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
class Migration12Test : AbstractMigrationTest() {
@Test
fun migrate11To12_twoNotRelatedStudents() {
fun twoNotRelatedStudents() {
helper.createDatabase(dbName, 11).apply {
// user 1
createStudent(this, 1, true)
@ -61,7 +45,7 @@ class Migration12Test {
}
@Test
fun migrate11To12_removeStudentsWithoutClassId() {
fun removeStudentsWithoutClassId() {
helper.createDatabase(dbName, 11).apply {
// user 1
createStudent(this, 1, true)
@ -85,7 +69,7 @@ class Migration12Test {
}
@Test
fun migrate11To12_ensureThereIsOnlyOneCurrentStudent() {
fun ensureThereIsOnlyOneCurrentStudent() {
helper.createDatabase(dbName, 11).apply {
// user 1
createStudent(this, 1, true)
@ -146,14 +130,4 @@ class Migration12Test {
put("unit_id", "99")
})
}
private fun getMigratedRoomDatabase(): AppDatabase {
val database = Room.databaseBuilder(ApplicationProvider.getApplicationContext(),
AppDatabase::class.java, dbName)
.addMigrations(Migration12())
.build()
// close the database and release any stream resources when the test finishes
helper.closeWhenFinished(database)
return database
}
}

View File

@ -0,0 +1,171 @@
package io.github.wulkanowy.data.db.migrations
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test
import org.threeten.bp.LocalDate.of
import kotlin.test.assertTrue
class Migration13Test : AbstractMigrationTest() {
@Test
fun studentsWithSchoolNameWithClassName() {
helper.createDatabase(dbName, 12).apply {
createStudent(this, 1, "Klasa A - Publiczna szkoła Wulkanowego nr 1 w fakelog.cf", 1, 1)
createStudent(this, 2, "Klasa B - Publiczna szkoła Wulkanowego-fejka nr 1 w fakelog.cf", 2, 1)
createStudent(this, 2, "Klasa C - Publiczna szkoła Wulkanowego-fejka nr 2 w fakelog.cf", 1, 2)
close()
}
helper.runMigrationsAndValidate(dbName, 13, true, Migration13())
val db = getMigratedRoomDatabase()
val students = db.studentDao.loadAll().blockingGet()
assertEquals(3, students.size)
students[0].run {
assertEquals(1, studentId)
assertEquals("A", className)
assertEquals("Publiczna szkoła Wulkanowego nr 1 w fakelog.cf", schoolName)
}
students[1].run {
assertEquals(2, studentId)
assertEquals("B", className)
assertEquals("Publiczna szkoła Wulkanowego-fejka nr 1 w fakelog.cf", schoolName)
}
students[2].run {
assertEquals(2, studentId)
assertEquals("C", className)
assertEquals("Publiczna szkoła Wulkanowego-fejka nr 2 w fakelog.cf", schoolName)
}
}
@Test
fun studentsWithSchoolNameWithoutClassName() {
helper.createDatabase(dbName, 12).apply {
createStudent(this, 1, "Publiczna szkoła Wulkanowego nr 1 w fakelog.cf", 1)
createStudent(this, 2, "Publiczna szkoła Wulkanowego-fejka nr 1 w fakelog.cf", 1)
close()
}
helper.runMigrationsAndValidate(dbName, 13, true, Migration13())
val db = getMigratedRoomDatabase()
val students = db.studentDao.loadAll().blockingGet()
assertEquals(2, students.size)
students[0].run {
assertEquals(1, studentId)
assertEquals("", className)
assertEquals("Publiczna szkoła Wulkanowego nr 1 w fakelog.cf", schoolName)
}
students[1].run {
assertEquals(2, studentId)
assertEquals("", className)
assertEquals("Publiczna szkoła Wulkanowego-fejka nr 1 w fakelog.cf", schoolName)
}
}
@Test
fun markAtLeastAndOnlyOneSemesterAtCurrent() {
helper.createDatabase(dbName, 12).apply {
createStudent(this, 1, "", 5)
createSemester(this, 1, 5, 1, 1, false)
createSemester(this, 1, 5, 2, 1, false)
createSemester(this, 1, 5, 3, 2, false)
createSemester(this, 1, 5, 4, 2, false)
createStudent(this, 2, "", 5)
createSemester(this, 2, 5, 5, 5, true)
createSemester(this, 2, 5, 6, 5, true)
createSemester(this, 2, 5, 7, 55, true)
createSemester(this, 2, 5, 8, 55, true)
createStudent(this, 3, "", 5)
createSemester(this, 3, 5, 11, 99, false)
createSemester(this, 3, 5, 12, 99, false)
createSemester(this, 3, 5, 13, 100, false)
createSemester(this, 3, 5, 14, 100, true)
close()
}
helper.runMigrationsAndValidate(dbName, 13, true, Migration13())
val db = getMigratedRoomDatabase()
val semesters1 = db.semesterDao.loadAll(1, 5).blockingGet()
assertTrue { semesters1.single { it.isCurrent }.isCurrent }
semesters1[0].run {
assertFalse(isCurrent)
assertEquals(1, semesterId)
assertEquals(1, diaryId)
}
semesters1[2].run {
assertFalse(isCurrent)
assertEquals(3, semesterId)
assertEquals(2, diaryId)
}
semesters1[3].run {
assertTrue(isCurrent)
assertEquals(4, semesterId)
assertEquals(2, diaryId)
}
db.semesterDao.loadAll(2, 5).blockingGet().let {
assertTrue { it.single { it.isCurrent }.isCurrent }
assertEquals(1970, it[0].schoolYear)
assertEquals(of(1970, 1, 1), it[0].end)
assertEquals(of(1970, 1, 1), it[0].start)
assertFalse(it[0].isCurrent)
assertFalse(it[1].isCurrent)
assertFalse(it[2].isCurrent)
assertTrue(it[3].isCurrent)
}
db.semesterDao.loadAll(2, 5).blockingGet().let {
assertTrue { it.single { it.isCurrent }.isCurrent }
assertFalse(it[0].isCurrent)
assertFalse(it[1].isCurrent)
assertFalse(it[2].isCurrent)
assertTrue(it[3].isCurrent)
}
}
private fun createStudent(db: SupportSQLiteDatabase, studentId: Int, schoolName: String = "", classId: Int = -1, schoolId: Int = 123) {
db.insert("Students", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
put("endpoint", "https://fakelog.cf")
put("loginType", "STANDARD")
put("email", "jan@fakelog.cf")
put("password", "******")
put("symbol", "Default")
put("student_id", studentId)
put("class_id", classId)
put("student_name", "Jan Kowalski")
put("school_id", schoolId)
put("school_name", schoolName)
put("is_current", false)
put("registration_date", "0")
})
}
private fun createSemester(db: SupportSQLiteDatabase, studentId: Int, classId: Int, semesterId: Int, diaryId: Int, isCurrent: Boolean = false) {
db.insert("Semesters", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
put("student_id", studentId)
put("diary_id", diaryId)
put("diary_name", "IA")
put("semester_id", semesterId)
put("semester_name", "1")
put("is_current", isCurrent)
put("class_id", classId)
put("unit_id", "99")
})
}
}

View File

@ -11,6 +11,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class)
@ -40,7 +41,7 @@ class AttendanceLocalTest {
))
val attendance = attendanceLocal
.getAttendance(Semester(1, 2, "", 1, 3, true, 1, 1),
.getAttendance(Semester(1, 2, "", 1, 3, 2019, true, now(), now(), 1, 1),
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 14)
)

View File

@ -41,7 +41,7 @@ class CompletedLessonsLocalTest {
))
val completed = completedLessonsLocal
.getCompletedLessons(Semester(1, 2, "", 1, 3, true, 1, 1),
.getCompletedLessons(Semester(1, 2, "", 1, 3, 2019, true, LocalDate.now(), LocalDate.now(), 1, 1),
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 14)
)

View File

@ -40,7 +40,7 @@ class ExamLocalTest {
))
val exams = examLocal
.getExams(Semester(1, 2, "", 1, 3, true, 1, 1),
.getExams(Semester(1, 2, "", 1, 3, 2019, true, LocalDate.now(), LocalDate.now(), 1, 1),
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 14)
)

View File

@ -10,6 +10,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class)
@ -39,8 +40,10 @@ class GradeLocalTest {
createGradeLocal(3, 5.0, LocalDate.of(2019, 2, 28), "", 2)
))
val semester = Semester(1, 2, "", 2019, 2, 1, true, now(), now(), 1, 1)
val grades = gradeLocal
.getGrades(Semester(1, 2, "", 2, 3, true, 1, 1))
.getGrades(semester)
.blockingGet()
assertEquals(2, grades.size)

View File

@ -22,6 +22,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.threeten.bp.LocalDate.of
import org.threeten.bp.LocalDateTime
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import io.github.wulkanowy.api.grades.Grade as GradeApi
@ -109,4 +110,73 @@ class GradeRepositoryTest {
assertTrue { grades[2].isRead }
assertTrue { grades[3].isRead }
}
@Test
fun subtractLocaleDuplicateGrades() {
gradeLocal.saveGrades(listOf(
createGradeLocal(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeLocal(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeLocal(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
every { mockApi.getGrades(1) } returns Single.just(listOf(
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(2, grades.size)
}
@Test
fun subtractRemoteDuplicateGrades() {
gradeLocal.saveGrades(listOf(
createGradeLocal(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeLocal(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
every { mockApi.getGrades(1) } returns Single.just(listOf(
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(3, grades.size)
}
@Test
fun emptyLocal() {
gradeLocal.saveGrades(listOf())
every { mockApi.getGrades(1) } returns Single.just(listOf(
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeApi(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(3, grades.size)
}
@Test
fun emptyRemote() {
gradeLocal.saveGrades(listOf(
createGradeLocal(5, 3.0, of(2019, 2, 25), "Taka sama ocena"),
createGradeLocal(3, 5.0, of(2019, 2, 26), "Jakaś inna ocena")
))
every { mockApi.getGrades(1) } returns Single.just(listOf())
val grades = GradeRepository(settings, gradeLocal, gradeRemote)
.getGrades(studentMock, semesterMock, true).blockingGet()
assertEquals(0, grades.size)
}
}

View File

@ -10,6 +10,7 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.threeten.bp.LocalDate
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class)
@ -39,7 +40,7 @@ class GradeStatisticsLocalTest {
))
val stats = gradeStatisticsLocal.getGradesStatistics(
Semester(2, 2, "", 1, 2, true, 1, 1), false,
Semester(2, 2, "", 2019, 1, 2, true, LocalDate.now(), LocalDate.now(), 1, 1), false,
"Matematyka"
).blockingGet()
assertEquals(1, stats.size)
@ -55,7 +56,7 @@ class GradeStatisticsLocalTest {
))
val stats = gradeStatisticsLocal.getGradesStatistics(
Semester(2, 2, "", 1, 2, true, 1, 1), false,
Semester(2, 2, "", 2019, 1, 2, true, LocalDate.now(), LocalDate.now(), 1, 1), false,
"Wszystkie"
).blockingGet()
assertEquals(1, stats.size)

View File

@ -36,7 +36,7 @@ class LuckyNumberLocalTest {
fun saveAndReadTest() {
luckyNumberLocal.saveLuckyNumber(LuckyNumber(1, LocalDate.of(2019, 1, 20), 14))
val luckyNumber = luckyNumberLocal.getLuckyNumber(Semester(1, 1, "", 1, 3, true, 1, 1),
val luckyNumber = luckyNumberLocal.getLuckyNumber(Semester(1, 1, "", 1, 3, 2019, true, LocalDate.now(), LocalDate.now(), 1, 1),
LocalDate.of(2019, 1, 20)
).blockingGet()

View File

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

View File

@ -39,7 +39,7 @@ class StudentLocalTest {
@Test
fun saveAndReadTest() {
studentLocal.saveStudent(Student(email = "test", password = "test123", schoolSymbol = "23", endpoint = "fakelog.cf", loginType = "AUTO", isCurrent = true, studentName = "", schoolName = "", studentId = 0, classId = 1, symbol = "", registrationDate = now()))
studentLocal.saveStudents(listOf(Student(email = "test", password = "test123", schoolSymbol = "23", endpoint = "fakelog.cf", loginType = "AUTO", isCurrent = true, studentName = "", schoolName = "", studentId = 0, classId = 1, symbol = "", registrationDate = now(), className = "")))
.blockingGet()
val student = studentLocal.getCurrentStudent(true).blockingGet()

View File

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

View File

@ -34,6 +34,7 @@
android:name=".ui.modules.login.LoginActivity"
android:configChanges="orientation|screenSize"
android:label="@string/login_title"
android:theme="@style/WulkanowyTheme.NoActionBar"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".ui.modules.main.MainActivity"
@ -45,13 +46,31 @@
android:configChanges="orientation|screenSize"
android:label="@string/send_message_title"
android:theme="@style/WulkanowyTheme.NoActionBar" />
<activity
android:name=".ui.modules.timetablewidget.TimetableWidgetConfigureActivity"
android:excludeFromRecents="true"
android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity"
android:excludeFromRecents="true"
android:noHistory="true"
android:theme="@style/WulkanowyTheme.WidgetAccountSwitcher">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<service
android:name=".services.widgets.TimetableWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<receiver
android:name=".ui.widgets.timetable.TimetableWidgetProvider"
android:name=".ui.modules.timetablewidget.TimetableWidgetProvider"
android:exported="true"
android:label="@string/timetable_title">
<intent-filter>
@ -62,6 +81,17 @@
android:resource="@xml/provider_widget_timetable" />
</receiver>
<receiver
android:name=".ui.modules.luckynumberwidget.LuckyNumberWidgetProvider"
android:label="@string/lucky_number_title">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/provider_widget_lucky_number" />
</receiver>
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"

View File

@ -1,7 +1,6 @@
package io.github.wulkanowy
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDex
import androidx.work.Configuration
import androidx.work.WorkManager
@ -13,24 +12,21 @@ import dagger.android.support.DaggerApplication
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.utils.Log
import io.fabric.sdk.android.Fabric
import io.github.wulkanowy.BuildConfig.CRASHLYTICS_ENABLED
import io.github.wulkanowy.BuildConfig.DEBUG
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.di.DaggerAppComponent
import io.github.wulkanowy.services.sync.SyncWorkerFactory
import io.github.wulkanowy.utils.ActivityLifecycleLogger
import io.github.wulkanowy.utils.CrashlyticsTree
import io.github.wulkanowy.utils.DebugLogTree
import io.reactivex.exceptions.UndeliverableException
import io.reactivex.plugins.RxJavaPlugins
import timber.log.Timber
import java.io.IOException
import java.lang.Exception
import javax.inject.Inject
class WulkanowyApp : DaggerApplication() {
@Inject
lateinit var prefRepository: PreferencesRepository
@Inject
lateinit var workerFactory: SyncWorkerFactory
@ -42,32 +38,36 @@ class WulkanowyApp : DaggerApplication() {
override fun onCreate() {
super.onCreate()
AndroidThreeTen.init(this)
initializeFabric()
if (DEBUG) enableDebugLog()
AppCompatDelegate.setDefaultNightMode(prefRepository.currentTheme)
WorkManager.initialize(this, Configuration.Builder().setWorkerFactory(workerFactory).build())
RxJavaPlugins.setErrorHandler(::onError)
initCrashlytics()
initLogging()
}
private fun enableDebugLog() {
Timber.plant(DebugLogTree())
FlexibleAdapter.enableLogs(Log.Level.DEBUG)
private fun initLogging() {
if (DEBUG) {
Timber.plant(DebugLogTree())
FlexibleAdapter.enableLogs(Log.Level.DEBUG)
} else {
Timber.plant(CrashlyticsTree())
}
registerActivityLifecycleCallbacks(ActivityLifecycleLogger())
}
private fun initializeFabric() {
private fun initCrashlytics() {
Fabric.with(Fabric.Builder(this).kits(
Crashlytics.Builder().core(CrashlyticsCore.Builder().disabled(!BuildConfig.CRASHLYTICS_ENABLED).build()).build()
).debuggable(BuildConfig.DEBUG).build())
Timber.plant(CrashlyticsTree())
Crashlytics.Builder().core(CrashlyticsCore.Builder().disabled(!CRASHLYTICS_ENABLED).build()).build()
).debuggable(DEBUG).build())
}
private fun onError(t: Throwable) {
if (t is UndeliverableException && t.cause is IOException || t.cause is InterruptedException) {
Timber.e(t.cause, "An undeliverable error occurred")
} else throw t
private fun onError(error: Throwable) {
if (error is UndeliverableException && error.cause is IOException || error.cause is InterruptedException) {
Timber.e(error.cause, "An undeliverable error occurred")
} else throw error
}
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.builder().create(this)
return DaggerAppComponent.factory().create(this)
}
}

View File

@ -43,6 +43,7 @@ import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.migrations.Migration10
import io.github.wulkanowy.data.db.migrations.Migration11
import io.github.wulkanowy.data.db.migrations.Migration12
import io.github.wulkanowy.data.db.migrations.Migration13
import io.github.wulkanowy.data.db.migrations.Migration2
import io.github.wulkanowy.data.db.migrations.Migration3
import io.github.wulkanowy.data.db.migrations.Migration4
@ -81,7 +82,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 12
const val VERSION_SCHEMA = 13
fun newInstance(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database")
@ -99,7 +100,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration9(),
Migration10(),
Migration11(),
Migration12()
Migration12(),
Migration13()
)
.build()
}

View File

@ -6,18 +6,16 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
@SuppressLint("ApplySharedPref")
class SharedPrefHelper @Inject constructor(private val sharedPref: SharedPreferences) {
@SuppressLint("ApplySharedPref")
fun putLong(key: String, value: Long, sync: Boolean = false) {
sharedPref.edit().putLong(key, value).apply {
if (sync) commit() else apply()
}
}
fun getLong(key: String, defaultValue: Long): Long {
return sharedPref.getLong(key, defaultValue)
}
fun getLong(key: String, defaultValue: Long) = sharedPref.getLong(key, defaultValue)
fun delete(key: String) {
sharedPref.edit().remove(key).apply()

View File

@ -14,7 +14,7 @@ import javax.inject.Singleton
interface StudentDao {
@Insert(onConflict = ABORT)
fun insert(student: Student): Long
fun insertAll(student: List<Student>): List<Long>
@Delete
fun delete(student: Student)

View File

@ -4,6 +4,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import org.threeten.bp.LocalDate
@Entity(tableName = "Semesters", indices = [Index(value = ["student_id", "diary_id", "semester_id"], unique = true)])
data class Semester(
@ -17,6 +18,9 @@ data class Semester(
@ColumnInfo(name = "diary_name")
val diaryName: String,
@ColumnInfo(name = "school_year")
val schoolYear: Int,
@ColumnInfo(name = "semester_id")
val semesterId: Int,
@ -26,6 +30,10 @@ data class Semester(
@ColumnInfo(name = "is_current")
val isCurrent: Boolean,
val start: LocalDate,
val end: LocalDate,
@ColumnInfo(name = "class_id")
val classId: Int,

View File

@ -32,6 +32,9 @@ data class Student(
@ColumnInfo(name = "school_name")
val schoolName: String,
@ColumnInfo(name = "class_name")
val className: String,
@ColumnInfo(name = "class_id")
val classId: Int,

View File

@ -8,7 +8,7 @@ class Migration11 : Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
CREATE TABLE IF NOT EXISTS Grades_temp (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
id INTEGER PRIMARY KEY NOT NULL,
is_read INTEGER NOT NULL,
is_notified INTEGER NOT NULL,
semester_id INTEGER NOT NULL,

View File

@ -0,0 +1,64 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration13 : Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) {
addClassNameToStudents(database, getStudentsIds(database))
updateSemestersTable(database)
markAtLeastAndOnlyOneSemesterAtCurrent(database, getStudentsAndClassIds(database))
clearMessagesTable(database)
}
private fun addClassNameToStudents(database: SupportSQLiteDatabase, students: List<Pair<Int, String>>) {
database.execSQL("ALTER TABLE Students ADD COLUMN class_name TEXT DEFAULT \"\" NOT NULL")
students.forEach { (id, name) ->
val schoolName = name.substringAfter(" - ")
val className = name.substringBefore(" - ", "").replace("Klasa ", "")
database.execSQL("UPDATE Students SET class_name = '$className' WHERE id = '$id'")
database.execSQL("UPDATE Students SET school_name = '$schoolName' WHERE id = '$id'")
}
}
private fun getStudentsIds(database: SupportSQLiteDatabase): MutableList<Pair<Int, String>> {
val students = mutableListOf<Pair<Int, String>>()
val studentsCursor = database.query("SELECT id, school_name FROM Students")
if (studentsCursor.moveToFirst()) {
do {
students.add(studentsCursor.getInt(0) to studentsCursor.getString(1))
} while (studentsCursor.moveToNext())
}
return students
}
private fun updateSemestersTable(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Semesters ADD COLUMN school_year INTEGER DEFAULT 1970 NOT NULL")
database.execSQL("ALTER TABLE Semesters ADD COLUMN start INTEGER DEFAULT 0 NOT NULL")
database.execSQL("ALTER TABLE Semesters ADD COLUMN `end` INTEGER DEFAULT 0 NOT NULL")
}
private fun getStudentsAndClassIds(database: SupportSQLiteDatabase): List<Pair<Int, Int>> {
val students = mutableListOf<Pair<Int, Int>>()
val studentsCursor = database.query("SELECT student_id, class_id FROM Students")
if (studentsCursor.moveToFirst()) {
do {
students.add(studentsCursor.getInt(0) to studentsCursor.getInt(1))
} while (studentsCursor.moveToNext())
}
return students
}
private fun markAtLeastAndOnlyOneSemesterAtCurrent(database: SupportSQLiteDatabase, students: List<Pair<Int, Int>>) {
students.forEach { (studentId, classId) ->
database.execSQL("UPDATE Semesters SET is_current = 0 WHERE student_id = '$studentId' AND class_id = '$classId'")
database.execSQL("UPDATE Semesters SET is_current = 1 WHERE id = (SELECT id FROM Semesters WHERE student_id = '$studentId' AND class_id = '$classId' ORDER BY semester_id DESC)")
}
}
private fun clearMessagesTable(database: SupportSQLiteDatabase) {
database.execSQL("DELETE FROM Messages")
}
}

View File

@ -6,6 +6,7 @@ import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
import org.threeten.bp.LocalDate
import java.net.UnknownHostException
@ -31,8 +32,8 @@ class AttendanceRepository @Inject constructor(
local.getAttendance(semester, dates.first, dates.second)
.toSingle(emptyList())
.doOnSuccess { oldAttendance ->
local.deleteAttendance(oldAttendance - newAttendance)
local.saveAttendance(newAttendance - oldAttendance)
local.deleteAttendance(oldAttendance.uniqueSubtract(newAttendance))
local.saveAttendance(newAttendance.uniqueSubtract(oldAttendance))
}
}.flatMap {
local.getAttendance(semester, dates.first, dates.second)

View File

@ -4,6 +4,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
@ -25,8 +26,8 @@ class AttendanceSummaryRepository @Inject constructor(
}.flatMap { new ->
local.getAttendanceSummary(semester, subjectId).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteAttendanceSummary(old - new)
local.saveAttendanceSummary(new - old)
local.deleteAttendanceSummary(old.uniqueSubtract(new))
local.saveAttendanceSummary(new.uniqueSubtract(old))
}
}.flatMap { local.getAttendanceSummary(semester, subjectId).toSingle(emptyList()) })
}

View File

@ -6,6 +6,7 @@ import io.github.wulkanowy.data.db.entities.CompletedLesson
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
import org.threeten.bp.LocalDate
import java.net.UnknownHostException
@ -31,8 +32,8 @@ class CompletedLessonsRepository @Inject constructor(
local.getCompletedLessons(semester, dates.first, dates.second)
.toSingle(emptyList())
.doOnSuccess { old ->
local.deleteCompleteLessons(old - new)
local.saveCompletedLessons(new - old)
local.deleteCompleteLessons(old.uniqueSubtract(new))
local.saveCompletedLessons(new.uniqueSubtract(old))
}
}.flatMap {
local.getCompletedLessons(semester, dates.first, dates.second)

View File

@ -6,6 +6,7 @@ import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
import org.threeten.bp.LocalDate
import java.net.UnknownHostException
@ -27,12 +28,12 @@ class ExamRepository @Inject constructor(
.flatMap {
if (it) remote.getExams(semester, dates.first, dates.second)
else Single.error(UnknownHostException())
}.flatMap { newExams ->
}.flatMap { new ->
local.getExams(semester, dates.first, dates.second)
.toSingle(emptyList())
.doOnSuccess { oldExams ->
local.deleteExams(oldExams - newExams)
local.saveExams(newExams - oldExams)
.doOnSuccess { old ->
local.deleteExams(old.uniqueSubtract(new))
local.saveExams(new.uniqueSubtract(old))
}
}.flatMap {
local.getExams(semester, dates.first, dates.second)

View File

@ -5,6 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Completable
import io.reactivex.Single
import java.net.UnknownHostException
@ -24,13 +25,12 @@ class GradeRepository @Inject constructor(
.flatMap {
if (it) remote.getGrades(semester)
else Single.error(UnknownHostException())
}.flatMap { newGrades ->
}.flatMap { new ->
local.getGrades(semester).toSingle(emptyList())
.doOnSuccess { oldGrades ->
val notifyBreakDate = oldGrades.maxBy { it.date }?.date
?: student.registrationDate.toLocalDate()
local.deleteGrades(oldGrades - newGrades)
local.saveGrades((newGrades - oldGrades)
.doOnSuccess { old ->
val notifyBreakDate = old.maxBy { it.date }?.date ?: student.registrationDate.toLocalDate()
local.deleteGrades(old.uniqueSubtract(new))
local.saveGrades(new.uniqueSubtract(old)
.onEach {
if (it.date >= notifyBreakDate) it.apply {
isRead = false

View File

@ -4,6 +4,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
@ -22,11 +23,11 @@ class GradeSummaryRepository @Inject constructor(
.flatMap {
if (it) remote.getGradeSummary(semester)
else Single.error(UnknownHostException())
}.flatMap { newGradesSummary ->
}.flatMap { new ->
local.getGradesSummary(semester).toSingle(emptyList())
.doOnSuccess { oldGradesSummary ->
local.deleteGradesSummary(oldGradesSummary - newGradesSummary)
local.saveGradesSummary(newGradesSummary - oldGradesSummary)
.doOnSuccess { old ->
local.deleteGradesSummary(old.uniqueSubtract(new))
local.saveGradesSummary(new.uniqueSubtract(old))
}
}.flatMap { local.getGradesSummary(semester).toSingle(emptyList()) })
}

View File

@ -4,6 +4,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
@ -22,11 +23,11 @@ class GradeStatisticsRepository @Inject constructor(
.flatMap {
if (it) remote.getGradeStatistics(semester, isSemester)
else Single.error(UnknownHostException())
}.flatMap { newGradesStats ->
}.flatMap { new ->
local.getGradesStatistics(semester, isSemester).toSingle(emptyList())
.doOnSuccess { oldGradesStats ->
local.deleteGradesStatistics(oldGradesStats - newGradesStats)
local.saveGradesStatistics(newGradesStats - oldGradesStats)
.doOnSuccess { old ->
local.deleteGradesStatistics(old.uniqueSubtract(new))
local.saveGradesStatistics(new.uniqueSubtract(old))
}
}.flatMap { local.getGradesStatistics(semester, isSemester, subjectName).toSingle(emptyList()) })
}

View File

@ -6,6 +6,7 @@ import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
import org.threeten.bp.LocalDate
import java.net.UnknownHostException
@ -26,11 +27,11 @@ class HomeworkRepository @Inject constructor(
.flatMap {
if (it) remote.getHomework(semester, monday, friday)
else Single.error(UnknownHostException())
}.flatMap { newGrades ->
}.flatMap { new ->
local.getHomework(semester, monday, friday).toSingle(emptyList())
.doOnSuccess { oldGrades ->
local.deleteHomework(oldGrades - newGrades)
local.saveHomework(newGrades - oldGrades)
.doOnSuccess { old ->
local.deleteHomework(old.uniqueSubtract(new))
local.saveHomework(new.uniqueSubtract(old))
}
}.flatMap { local.getHomework(semester, monday, friday).toSingle(emptyList()) })
}

View File

@ -24,13 +24,13 @@ class MessageLocal @Inject constructor(private val messagesDb: MessagesDao) {
}
fun getMessage(student: Student, id: Int): Maybe<Message> {
return messagesDb.load(student.studentId, id)
return messagesDb.load(student.id.toInt(), id)
}
fun getMessages(student: Student, folder: MessageFolder): Maybe<List<Message>> {
return when (folder) {
TRASHED -> messagesDb.loadDeleted(student.studentId)
else -> messagesDb.loadAll(student.studentId, folder.id)
TRASHED -> messagesDb.loadDeleted(student.id.toInt())
else -> messagesDb.loadAll(student.id.toInt(), folder.id)
}.filter { it.isNotEmpty() }
}
}

View File

@ -5,6 +5,7 @@ import io.github.wulkanowy.api.messages.Folder
import io.github.wulkanowy.api.messages.SentMessage
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.toLocalDateTime
import io.reactivex.Single
import org.threeten.bp.LocalDateTime.now
@ -16,11 +17,11 @@ import io.github.wulkanowy.api.messages.Recipient as ApiRecipient
@Singleton
class MessageRemote @Inject constructor(private val api: Api) {
fun getMessages(studentId: Int, folder: MessageFolder): Single<List<Message>> {
fun getMessages(student: Student, folder: MessageFolder): Single<List<Message>> {
return api.getMessages(Folder.valueOf(folder.name)).map { messages ->
messages.map {
Message(
studentId = studentId,
studentId = student.id.toInt(),
realId = it.id ?: 0,
messageId = it.messageId ?: 0,
sender = it.sender.orEmpty(),
@ -58,4 +59,8 @@ class MessageRemote @Inject constructor(private val api: Api) {
}
)
}
fun deleteMessage(message: Message): Single<Boolean> {
return api.deleteMessages(listOf(Pair(message.realId, message.folderId)))
}
}

View File

@ -8,7 +8,9 @@ import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.message.MessageFolder.RECEIVED
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Completable
import io.reactivex.Maybe
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
@ -28,13 +30,13 @@ class MessageRepository @Inject constructor(
local.getMessages(student, folder).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getMessages(student.studentId, folder)
if (it) remote.getMessages(student, folder)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getMessages(student, folder).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteMessages(old - new)
local.saveMessages((new - old)
local.deleteMessages(old.uniqueSubtract(new))
local.saveMessages(new.uniqueSubtract(old)
.onEach {
it.isNotified = !notify
})
@ -89,4 +91,20 @@ class MessageRepository @Inject constructor(
else Single.error(UnknownHostException())
}
}
fun deleteMessage(message: Message): Maybe<Boolean> {
return ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.deleteMessage(message)
else Single.error(UnknownHostException())
}
.filter { it }
.doOnSuccess {
if (!message.removed) local.updateMessages(listOf(message.copy(removed = true).apply {
id = message.id
content = message.content
}))
else local.deleteMessages(listOf(message))
}
}
}

View File

@ -5,6 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Completable
import io.reactivex.Single
import java.net.UnknownHostException
@ -27,8 +28,8 @@ class NoteRepository @Inject constructor(
}.flatMap { new ->
local.getNotes(student).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteNotes(old - new)
local.saveNotes((new - old)
local.deleteNotes(old.uniqueSubtract(new))
local.saveNotes(new.uniqueSubtract(old)
.onEach {
if (it.date >= student.registrationDate.toLocalDate()) it.apply {
isRead = false

View File

@ -17,12 +17,15 @@ class PreferencesRepository @Inject constructor(
val isShowPresent: Boolean
get() = sharedPref.getBoolean(context.getString(R.string.pref_key_attendance_present), true)
val gradeAverageMode: String
get() = sharedPref.getString(context.getString(R.string.pref_key_grade_average_mode), "only_one_semester") ?: "only_one_semester"
val isGradeExpandable: Boolean
get() = !sharedPref.getBoolean(context.getString(R.string.pref_key_expand_grade), false)
val currentThemeKey: String = context.getString(R.string.pref_key_theme)
val currentTheme: Int
get() = sharedPref.getString(currentThemeKey, "1")?.toIntOrNull() ?: 1
val appThemeKey: String = context.getString(R.string.pref_key_app_theme)
val appTheme: String
get() = sharedPref.getString(appThemeKey, "light") ?: "light"
val gradeColorTheme: String
get() = sharedPref.getString(context.getString(R.string.pref_key_grade_color_scheme), "vulcan") ?: "vulcan"
@ -50,8 +53,7 @@ class PreferencesRepository @Inject constructor(
get() = sharedPref.getString(context.getString(R.string.pref_key_grade_modifier_plus), "0.0")?.toDouble() ?: 0.0
val gradeMinusModifier: Double
get() = sharedPref.getString(context.getString(R.string.pref_key_grade_modifier_minus), "0.0")?.toDouble()
?: 0.0
get() = sharedPref.getString(context.getString(R.string.pref_key_grade_modifier_minus), "0.0")?.toDouble() ?: 0.0
val fillMessageContent: Boolean
get() = sharedPref.getBoolean(context.getString(R.string.pref_key_fill_message_content), true)

View File

@ -7,6 +7,7 @@ import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
@ -31,8 +32,8 @@ class RecipientRepository @Inject constructor(
}.flatMap { new ->
local.getRecipients(student, role, unit).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteRecipients(old - new)
local.saveRecipients(new - old)
local.deleteRecipients(old.uniqueSubtract(new))
local.saveRecipients(new.uniqueSubtract(old))
}
}.flatMap {
local.getRecipients(student, role, unit).toSingle(emptyList())

View File

@ -5,6 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.ApiHelper
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Maybe
import io.reactivex.Single
import java.net.UnknownHostException
@ -30,8 +31,8 @@ class ReportingUnitRepository @Inject constructor(
}.flatMap { new ->
local.getReportingUnits(student).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteReportingUnits(old - new)
local.saveReportingUnits(new - old)
local.deleteReportingUnits(old.uniqueSubtract(new))
local.saveReportingUnits(new.uniqueSubtract(old))
}
}.flatMap { local.getReportingUnits(student).toSingle(emptyList()) }
)

View File

@ -17,9 +17,12 @@ class SemesterRemote @Inject constructor(private val api: Api) {
studentId = student.studentId,
diaryId = semester.diaryId,
diaryName = semester.diaryName,
schoolYear = semester.schoolYear,
semesterId = semester.semesterId,
semesterName = semester.semesterNumber,
isCurrent = semester.current,
start = semester.start,
end = semester.end,
classId = semester.classId,
unitId = semester.unitId
)

View File

@ -5,6 +5,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.ApiHelper
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Maybe
import io.reactivex.Single
import timber.log.Timber
@ -31,8 +32,8 @@ class SemesterRepository @Inject constructor(
if (currentSemesters.size == 1) {
local.getSemesters(student).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteSemesters(old - new)
local.saveSemesters(new - old)
local.deleteSemesters(old.uniqueSubtract(new))
local.saveSemesters(new.uniqueSubtract(old))
}
} else {
Timber.i("Current semesters list:\n${currentSemesters.joinToString(separator = "\n")}")

View File

@ -17,8 +17,8 @@ class StudentLocal @Inject constructor(
private val context: Context
) {
fun saveStudent(student: Student): Single<Long> {
return Single.fromCallable { studentDb.insert(student.copy(password = encrypt(student.password, context))) }
fun saveStudents(students: List<Student>): Single<List<Long>> {
return Single.fromCallable { studentDb.insertAll(students.map { it.copy(password = encrypt(it.password, context)) }) }
}
fun getStudents(decryptPass: Boolean): Maybe<List<Student>> {

View File

@ -21,6 +21,7 @@ class StudentRemote @Inject constructor(private val api: Api) {
studentName = student.studentName,
schoolSymbol = student.schoolSymbol,
schoolName = student.schoolName,
className = student.className,
classId = student.classId,
endpoint = endpoint,
loginType = student.loginType.name,

View File

@ -41,8 +41,8 @@ class StudentRepository @Inject constructor(
.toSingle()
}
fun saveStudent(student: Student): Single<Long> {
return local.saveStudent(student)
fun saveStudents(students: List<Student>): Single<List<Long>> {
return local.saveStudents(students)
}
fun switchStudent(student: Student): Completable {

View File

@ -4,6 +4,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
@ -26,8 +27,8 @@ class SubjectRepository @Inject constructor(
local.getSubjects(semester)
.toSingle(emptyList())
.doOnSuccess { old ->
local.deleteSubjects(old - new)
local.saveSubjects(new - old)
local.deleteSubjects(old.uniqueSubtract(new))
local.saveSubjects(new.uniqueSubtract(old))
}
}.flatMap {
local.getSubjects(semester).toSingle(emptyList())

View File

@ -6,6 +6,7 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Single
import org.threeten.bp.LocalDate
import java.net.UnknownHostException
@ -25,14 +26,14 @@ class TimetableRepository @Inject constructor(
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getTimetable(semester, monday, friday)
else Single.error(UnknownHostException())
}.flatMap { newTimetable ->
}.flatMap { new ->
local.getTimetable(semester, monday, friday)
.toSingle(emptyList())
.doOnSuccess { oldTimetable ->
local.deleteTimetable(oldTimetable - newTimetable)
local.saveTimetable((newTimetable - oldTimetable).map { item ->
.doOnSuccess { old ->
local.deleteTimetable(old.uniqueSubtract(new))
local.saveTimetable(new.uniqueSubtract(old).map { item ->
item.apply {
oldTimetable.singleOrNull { this.start == it.start }?.let {
old.singleOrNull { this.start == it.start }?.let {
return@map copy(
room = if (room.isEmpty()) it.room else room,
teacher = if (teacher.isEmpty()) it.teacher else teacher

View File

@ -17,6 +17,6 @@ import javax.inject.Singleton
BuilderModule::class])
interface AppComponent : AndroidInjector<WulkanowyApp> {
@Component.Builder
abstract class Builder : AndroidInjector.Builder<WulkanowyApp>()
@Component.Factory
interface Factory : AndroidInjector.Factory<WulkanowyApp>
}

View File

@ -9,7 +9,6 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.BuildConfig.DEBUG
import io.github.wulkanowy.WulkanowyApp
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import javax.inject.Named
import javax.inject.Singleton
@ -30,11 +29,11 @@ internal class AppModule {
@Singleton
@Provides
fun provideFirebaseAnalyticsHelper(context: Context) = FirebaseAnalyticsHelper(FirebaseAnalytics.getInstance(context))
fun provideFirebaseAnalytics(context: Context) = FirebaseAnalytics.getInstance(context)
@Singleton
@Provides
fun provideAppWidgetManager(context: Context) = AppWidgetManager.getInstance(context)
fun provideAppWidgetManager(context: Context): AppWidgetManager = AppWidgetManager.getInstance(context)
@Singleton
@Named("isDebug")

View File

@ -5,11 +5,14 @@ import dagger.android.ContributesAndroidInjector
import io.github.wulkanowy.di.scopes.PerActivity
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginModule
import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetConfigureActivity
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainModule
import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.ui.widgets.timetable.TimetableWidgetProvider
import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetConfigureActivity
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider
@Module
internal abstract class BuilderModule {
@ -29,6 +32,15 @@ internal abstract class BuilderModule {
@ContributesAndroidInjector
abstract fun bindMessageSendActivity(): SendMessageActivity
@ContributesAndroidInjector
abstract fun bindTimetableWidgetAccountActivity(): TimetableWidgetConfigureActivity
@ContributesAndroidInjector
abstract fun bindTimetableWidgetProvider(): TimetableWidgetProvider
@ContributesAndroidInjector
abstract fun bindLuckyNumberWidgetAccountActivity(): LuckyNumberWidgetConfigureActivity
@ContributesAndroidInjector
abstract fun bindLuckyNumberWidgetProvider(): LuckyNumberWidgetProvider
}

View File

@ -15,7 +15,7 @@ import io.github.wulkanowy.data.repositories.grade.GradeRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.NewEntriesChannel
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainActivity.Companion.EXTRA_START_MENU_INDEX
import io.github.wulkanowy.ui.modules.main.MainView.MenuView
import io.github.wulkanowy.utils.getCompatColor
import io.reactivex.Completable
import javax.inject.Inject
@ -47,8 +47,8 @@ class GradeWork @Inject constructor(
.setDefaults(DEFAULT_ALL)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(context, 0,
MainActivity.getStartIntent(context).putExtra(EXTRA_START_MENU_INDEX, 0), FLAG_UPDATE_CURRENT))
PendingIntent.getActivity(context, MenuView.GRADE.id,
MainActivity.getStartIntent(context, MenuView.GRADE, true), FLAG_UPDATE_CURRENT))
.setStyle(NotificationCompat.InboxStyle().run {
setSummaryText(context.resources.getQuantityString(R.plurals.grade_number_item, grades.size, grades.size))
grades.forEach { addLine("${it.subject}: ${it.entry}") }

View File

@ -15,7 +15,7 @@ import io.github.wulkanowy.data.repositories.luckynumber.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.NewEntriesChannel
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainActivity.Companion.EXTRA_START_MENU_INDEX
import io.github.wulkanowy.ui.modules.main.MainView.MenuView
import io.github.wulkanowy.utils.getCompatColor
import io.reactivex.Completable
import javax.inject.Inject
@ -47,9 +47,8 @@ class LuckyNumberWork @Inject constructor(
.setPriority(PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(context, 0,
MainActivity.getStartIntent(context).putExtra(EXTRA_START_MENU_INDEX, 4), FLAG_UPDATE_CURRENT)
)
PendingIntent.getActivity(context, MenuView.MESSAGE.id,
MainActivity.getStartIntent(context, MenuView.LUCKY_NUMBER, true), FLAG_UPDATE_CURRENT))
.build())
}
}

View File

@ -16,7 +16,7 @@ import io.github.wulkanowy.data.repositories.message.MessageRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.NewEntriesChannel
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainActivity.Companion.EXTRA_START_MENU_INDEX
import io.github.wulkanowy.ui.modules.main.MainView.MenuView
import io.github.wulkanowy.utils.getCompatColor
import io.reactivex.Completable
import javax.inject.Inject
@ -48,8 +48,8 @@ class MessageWork @Inject constructor(
.setPriority(PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(context, 0,
MainActivity.getStartIntent(context).putExtra(EXTRA_START_MENU_INDEX, 4), FLAG_UPDATE_CURRENT)
PendingIntent.getActivity(context, MenuView.MESSAGE.id,
MainActivity.getStartIntent(context, MenuView.MESSAGE, true), FLAG_UPDATE_CURRENT)
)
.setStyle(NotificationCompat.InboxStyle().run {
setSummaryText(context.resources.getQuantityString(R.plurals.message_number_item, messages.size, messages.size))

View File

@ -15,7 +15,7 @@ import io.github.wulkanowy.data.repositories.note.NoteRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.NewEntriesChannel
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainActivity.Companion.EXTRA_START_MENU_INDEX
import io.github.wulkanowy.ui.modules.main.MainView.MenuView
import io.github.wulkanowy.utils.getCompatColor
import io.reactivex.Completable
import javax.inject.Inject
@ -47,9 +47,8 @@ class NoteWork @Inject constructor(
.setPriority(PRIORITY_HIGH)
.setColor(context.getCompatColor(R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(context, 0,
MainActivity.getStartIntent(context).putExtra(EXTRA_START_MENU_INDEX, 4), FLAG_UPDATE_CURRENT)
)
PendingIntent.getActivity(context, MenuView.NOTE.id,
MainActivity.getStartIntent(context, MenuView.NOTE, true), FLAG_UPDATE_CURRENT))
.setStyle(NotificationCompat.InboxStyle().run {
setSummaryText(context.resources.getQuantityString(R.plurals.note_number_item, notes.size, notes.size))
notes.forEach { addLine("${it.teacher}: ${it.category}") }

View File

@ -7,7 +7,7 @@ import io.github.wulkanowy.data.db.SharedPrefHelper
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.data.repositories.timetable.TimetableRepository
import io.github.wulkanowy.ui.widgets.timetable.TimetableWidgetFactory
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetFactory
import io.github.wulkanowy.utils.SchedulersProvider
import javax.inject.Inject

View File

@ -2,33 +2,59 @@ package io.github.wulkanowy.ui.base
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import dagger.android.support.DaggerAppCompatActivity
import dagger.android.AndroidInjection
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import io.github.wulkanowy.R
import io.github.wulkanowy.utils.FragmentLifecycleLogger
import javax.inject.Inject
abstract class BaseActivity : DaggerAppCompatActivity(), BaseView {
abstract class BaseActivity : AppCompatActivity(), BaseView, HasSupportFragmentInjector {
protected lateinit var messageContainer: View
@Inject
lateinit var supportFragmentInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var fragmentLifecycleLogger: FragmentLifecycleLogger
@Inject
lateinit var themeManager: ThemeManager
protected var messageContainer: View? = null
public override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
themeManager.applyTheme(this)
super.onCreate(savedInstanceState)
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
}
override fun showError(text: String, error: Throwable) {
Snackbar.make(messageContainer, text, LENGTH_LONG).setAction(R.string.all_details) {
ErrorDialog.newInstance(error).show(supportFragmentManager, error.toString())
}.show()
if (messageContainer != null) {
Snackbar.make(messageContainer!!, text, LENGTH_LONG)
.setAction(R.string.all_details) {
ErrorDialog.newInstance(error).show(supportFragmentManager, error.toString())
}
.show()
} else showMessage(text)
}
override fun showMessage(text: String) {
Snackbar.make(messageContainer, text, LENGTH_LONG).show()
if (messageContainer != null) Snackbar.make(messageContainer!!, text, LENGTH_LONG).show()
else Toast.makeText(this, text, Toast.LENGTH_LONG).show()
}
override fun onDestroy() {
super.onDestroy()
invalidateOptionsMenu()
}
override fun supportFragmentInjector() = supportFragmentInjector
}

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.base
import android.view.View
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import dagger.android.support.DaggerFragment
import io.github.wulkanowy.R
@ -10,16 +11,22 @@ abstract class BaseFragment : DaggerFragment(), BaseView {
protected var messageContainer: View? = null
override fun showError(text: String, error: Throwable) {
if (messageContainer == null) (activity as? BaseActivity)?.showError(text, error)
else messageContainer?.also {
Snackbar.make(it, text, Snackbar.LENGTH_LONG).setAction(R.string.all_details) {
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}.show()
if (messageContainer != null) {
Snackbar.make(messageContainer!!, text, LENGTH_LONG)
.setAction(R.string.all_details) {
if (isAdded) ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
.show()
} else {
(activity as? BaseActivity)?.showError(text, error)
}
}
override fun showMessage(text: String) {
if (messageContainer == null) (activity as? BaseActivity)?.showMessage(text)
else messageContainer?.also { Snackbar.make(it, text, Snackbar.LENGTH_LONG).show() }
if (messageContainer != null) {
Snackbar.make(messageContainer!!, text, LENGTH_LONG).show()
} else {
(activity as? BaseActivity)?.showMessage(text)
}
}
}

View File

@ -0,0 +1,33 @@
package io.github.wulkanowy.ui.base
import android.content.pm.PackageManager.GET_ACTIVITIES
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import javax.inject.Inject
class ThemeManager @Inject constructor(private val preferencesRepository: PreferencesRepository) {
fun applyTheme(activity: AppCompatActivity) {
if (isThemeApplicable(activity)) {
activity.delegate.apply {
when (preferencesRepository.appTheme) {
"light" -> setLocalNightMode(MODE_NIGHT_NO)
"dark" -> setLocalNightMode(MODE_NIGHT_YES)
"black" -> {
setLocalNightMode(MODE_NIGHT_YES)
activity.setTheme(R.style.WulkanowyTheme_Black)
}
}
}
}
}
private fun isThemeApplicable(activity: AppCompatActivity): Boolean {
return activity.packageManager.getPackageInfo(activity.packageName, GET_ACTIVITIES)
.activities.singleOrNull { it.name == activity::class.java.canonicalName }?.theme
.let { it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar }
}
}

View File

@ -12,6 +12,7 @@ import io.github.wulkanowy.BuildConfig
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.withOnExtraListener
import javax.inject.Inject
@ -44,7 +45,7 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
.withFields(R.string::class.java.fields)
.withCheckCachedDetection(false)
.withExcludedLibraries("fastadapter", "AndroidIconics", "Jsoup", "Retrofit", "okio",
"OkHttp", "Butterknife", "CircleImageView")
"Butterknife", "CircleImageView")
.withOnExtraListener { presenter.onExtraSelect(it) })
}.let {
fragmentCompat.onCreateView(inflater.context, inflater, container, savedInstanceState, it)
@ -57,11 +58,11 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
}
override fun openDiscordInviteView() {
startActivity(Intent.parseUri("https://discord.gg/vccAQBr", 0))
context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage)
}
override fun openHomepageWebView() {
startActivity(Intent.parseUri("https://wulkanowy.github.io/", 0))
context?.openInternetBrowser("https://wulkanowy.github.io/", ::showMessage)
}
override fun openEmailClientView() {
@ -80,7 +81,7 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
if (intent.resolveActivity(it.packageManager) != null) {
startActivity(Intent.createChooser(intent, getString(R.string.about_feedback)))
} else {
startActivity(Intent.parseUri("https://github.com/wulkanowy/wulkanowy/issues", 0))
it.openInternetBrowser("https://github.com/wulkanowy/wulkanowy/issues", ::showMessage)
}
}
}

View File

@ -17,7 +17,7 @@ class AboutPresenter @Inject constructor(
override fun onAttachView(view: AboutView) {
super.onAttachView(view)
Timber.i("About view is attached")
Timber.i("About view was initialized")
}
fun onExtraSelect(type: Libs.SpecialButton?) {

View File

@ -15,7 +15,6 @@ import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.dialog_account.*
import javax.inject.Inject
@ -97,11 +96,8 @@ class AccountDialog : DaggerAppCompatDialogFragment(), AccountView {
}
}
override fun recreateView() {
activity?.also {
startActivity(MainActivity.getStartIntent(it))
it.finish()
}
override fun recreateMainView() {
activity?.recreate()
}
override fun onDestroy() {

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.account
import android.annotation.SuppressLint
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -18,9 +19,10 @@ class AccountItem(val student: Student) : AbstractFlexibleItem<AccountItem.ViewH
return ViewHolder(view, adapter)
}
@SuppressLint("SetTextI18n")
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
holder.apply {
accountItemName.text = student.studentName
accountItemName.text = "${student.studentName} ${student.className}"
accountItemSchool.text = student.schoolName
accountItemImage.setBackgroundResource(if (student.isCurrent) R.drawable.ic_account_circular_border else 0)
}

View File

@ -19,8 +19,8 @@ class AccountPresenter @Inject constructor(
override fun onAttachView(view: AccountView) {
super.onAttachView(view)
Timber.i("Account dialog is attached")
view.initView()
Timber.i("Account dialog view was initialized")
loadData()
}
@ -54,7 +54,7 @@ class AccountPresenter @Inject constructor(
openClearLoginView()
} else {
Timber.i("Logout result: Switch to another student")
recreateView()
recreateMainView()
}
}
}, {
@ -73,9 +73,10 @@ class AccountPresenter @Inject constructor(
disposable.add(studentRepository.switchStudent(item.student)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally { view?.dismissView() }
.subscribe({
Timber.i("Change a student result: Success")
view?.recreateView()
view?.recreateMainView()
}, {
Timber.i("Change a student result: An exception occurred")
errorHandler.dispatch(it)

View File

@ -16,6 +16,6 @@ interface AccountView : BaseView {
fun openClearLoginView()
fun recreateView()
fun recreateMainView()
}

View File

@ -77,12 +77,12 @@ class AttendanceFragment : BaseSessionFragment(), AttendanceView, MainView.MainC
attendanceNextButton.setOnClickListener { presenter.onNextDay() }
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.action_menu_attendance, menu)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.action_menu_attendance, menu)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
return if (item?.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected()
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected()
else false
}
@ -103,7 +103,7 @@ class AttendanceFragment : BaseSessionFragment(), AttendanceView, MainView.MainC
}
override fun onFragmentReselected() {
presenter.onViewReselected()
if (::presenter.isInitialized) presenter.onViewReselected()
}
override fun popView() {

View File

@ -37,8 +37,8 @@ class AttendancePresenter @Inject constructor(
fun onAttachView(view: AttendanceView, date: Long?) {
super.onAttachView(view)
Timber.i("Attendance view is attached")
view.initView()
Timber.i("Attendance view was initialized")
loadData(ofEpochDay(date ?: now().previousOrSameSchoolDay.toEpochDay()))
reloadView()
}

View File

@ -35,8 +35,8 @@ class AttendanceSummaryPresenter @Inject constructor(
fun onAttachView(view: AttendanceSummaryView, subjectId: Int?) {
super.onAttachView(view)
Timber.i("Attendance summary view is attached with subject id ${subjectId ?: -1}")
view.initView()
Timber.i("Attendance summary view was initialized with subject id ${subjectId ?: -1}")
loadData(subjectId ?: -1)
loadSubjects()
}

View File

@ -88,7 +88,7 @@ class ExamFragment : BaseSessionFragment(), ExamView, MainView.MainChildView, Ma
}
override fun onFragmentReselected() {
presenter.onViewReselected()
if (::presenter.isInitialized) presenter.onViewReselected()
}
override fun showEmpty(show: Boolean) {

View File

@ -36,8 +36,8 @@ class ExamPresenter @Inject constructor(
fun onAttachView(view: ExamView, date: Long?) {
super.onAttachView(view)
Timber.i("Exam view is attached")
view.initView()
Timber.i("Exam view was initialized")
loadData(ofEpochDay(date ?: now().nextOrSameSchoolDay.toEpochDay()))
reloadView()
}

View File

@ -0,0 +1,54 @@
package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.grade.GradeRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier
import io.reactivex.Single
import javax.inject.Inject
class GradeAverageProvider @Inject constructor(
private val preferencesRepository: PreferencesRepository,
private val gradeRepository: GradeRepository
) {
fun getGradeAverage(student: Student, semesters: List<Semester>, selectedSemesterId: Int, forceRefresh: Boolean): Single<Map<String, Double>> {
return when (preferencesRepository.gradeAverageMode) {
"all_year" -> getAllYearAverage(student, semesters, selectedSemesterId, forceRefresh)
"only_one_semester" -> getOnlyOneSemesterAverage(student, semesters, selectedSemesterId, forceRefresh)
else -> throw IllegalArgumentException("Incorrect grade average mode: ${preferencesRepository.gradeAverageMode} ")
}
}
private fun getAllYearAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<Map<String, Double>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
val plusModifier = preferencesRepository.gradePlusModifier
val minusModifier = preferencesRepository.gradeMinusModifier
return gradeRepository.getGrades(student, selectedSemester, forceRefresh)
.flatMap { firstGrades ->
if (selectedSemester == firstSemester) Single.just(firstGrades)
else gradeRepository.getGrades(student, firstSemester)
.map { secondGrades -> secondGrades + firstGrades }
}.map { grades ->
grades.map { it.changeModifier(plusModifier, minusModifier) }
.groupBy { it.subject }
.mapValues { it.value.calcAverage() }
}
}
private fun getOnlyOneSemesterAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Single<Map<String, Double>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
val plusModifier = preferencesRepository.gradePlusModifier
val minusModifier = preferencesRepository.gradeMinusModifier
return gradeRepository.getGrades(student, selectedSemester, forceRefresh)
.map { grades ->
grades.map { it.changeModifier(plusModifier, minusModifier) }
.groupBy { it.subject }
.mapValues { it.value.calcAverage() }
}
}
}

View File

@ -57,9 +57,9 @@ class GradeFragment : BaseSessionFragment(), GradeView, MainView.MainChildView,
presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SEMESTER_KEY))
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.action_menu_grade, menu)
semesterSwitchMenu = menu?.findItem(R.id.gradeMenuSemester)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.action_menu_grade, menu)
semesterSwitchMenu = menu.findItem(R.id.gradeMenuSemester)
presenter.onCreateMenu()
}
@ -82,13 +82,13 @@ class GradeFragment : BaseSessionFragment(), GradeView, MainView.MainChildView,
gradeSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
return if (item?.itemId == R.id.gradeMenuSemester) presenter.onSemesterSwitch()
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.gradeMenuSemester) presenter.onSemesterSwitch()
else false
}
override fun onFragmentReselected() {
presenter.onViewReselected()
if (::presenter.isInitialized) presenter.onViewReselected()
}
override fun showContent(show: Boolean) {

View File

@ -27,12 +27,12 @@ class GradePresenter @Inject constructor(
fun onAttachView(view: GradeView, savedIndex: Int?) {
super.onAttachView(view)
Timber.i("Grade view is attached")
selectedIndex = savedIndex ?: 0
view.run {
initView()
enableSwipe(false)
}
Timber.i("Grade view was initialized with $selectedIndex index")
loadData()
}

View File

@ -67,8 +67,8 @@ class GradeDetailsFragment : BaseSessionFragment(), GradeDetailsView, GradeView.
presenter.onAttachView(this)
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.action_menu_grade_details, menu)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.action_menu_grade_details, menu)
}
override fun initView() {
@ -88,8 +88,8 @@ class GradeDetailsFragment : BaseSessionFragment(), GradeDetailsView, GradeView.
gradeDetailsSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
return if (item?.itemId == R.id.gradeDetailsMenuRead) presenter.onMarkAsReadSelected()
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.gradeDetailsMenuRead) presenter.onMarkAsReadSelected()
else false
}

View File

@ -8,10 +8,9 @@ import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.session.BaseSessionPresenter
import io.github.wulkanowy.ui.base.session.SessionErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier
import io.github.wulkanowy.utils.getBackgroundColor
import timber.log.Timber
import javax.inject.Inject
@ -23,6 +22,7 @@ class GradeDetailsPresenter @Inject constructor(
private val studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository,
private val preferencesRepository: PreferencesRepository,
private val averageProvider: GradeAverageProvider,
private val analytics: FirebaseAnalyticsHelper
) : BaseSessionPresenter<GradeDetailsView>(errorHandler) {
@ -109,11 +109,16 @@ class GradeDetailsPresenter @Inject constructor(
private fun loadData(semesterId: Int, forceRefresh: Boolean) {
Timber.i("Loading grade details data started")
disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getSemesters(it).map { semester -> semester to it } }
.flatMap { gradeRepository.getGrades(it.second, it.first.first { item -> item.semesterId == semesterId }, forceRefresh) }
.map { it.sortedByDescending { grade -> grade.date } }
.map { it.map { item -> item.changeModifier(preferencesRepository.gradePlusModifier, preferencesRepository.gradeMinusModifier) } }
.map { createGradeItems(it.groupBy { grade -> grade.subject }.toSortedMap()) }
.flatMap { semesterRepository.getSemesters(it).map { semester -> it to semester } }
.flatMap { (student, semesters) ->
averageProvider.getGradeAverage(student, semesters, semesterId, forceRefresh)
.flatMap { averages ->
gradeRepository.getGrades(student, semesters.first { semester -> semester.semesterId == semesterId })
.map { it.sortedByDescending { grade -> grade.date } }
.map { it.groupBy { grade -> grade.subject }.toSortedMap() }
.map { createGradeItems(it, averages) }
}
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
@ -139,32 +144,36 @@ class GradeDetailsPresenter @Inject constructor(
})
}
private fun createGradeItems(items: Map<String, List<Grade>>): List<GradeDetailsHeader> {
private fun createGradeItems(items: Map<String, List<Grade>>, averages: Map<String, Double>): List<GradeDetailsHeader> {
val isGradeExpandable = preferencesRepository.isGradeExpandable
val gradeColorTheme = preferencesRepository.gradeColorTheme
val noDescriptionString = view?.noDescriptionString.orEmpty()
val weightString = view?.weightString.orEmpty()
return items.map {
it.value.calcAverage().let { average ->
GradeDetailsHeader(
subject = it.key,
average = formatAverage(average),
number = view?.getGradeNumberString(it.value.size).orEmpty(),
newGrades = it.value.filter { grade -> !grade.isRead }.size,
isExpandable = preferencesRepository.isGradeExpandable
).apply {
subItems = it.value.map { item ->
GradeDetailsItem(
grade = item,
valueBgColor = item.getBackgroundColor(preferencesRepository.gradeColorTheme),
weightString = view?.weightString.orEmpty(),
noDescriptionString = view?.noDescriptionString.orEmpty()
)
}
GradeDetailsHeader(
subject = it.key,
average = formatAverage(averages[it.key]),
number = view?.getGradeNumberString(it.value.size).orEmpty(),
newGrades = it.value.filter { grade -> !grade.isRead }.size,
isExpandable = isGradeExpandable
).apply {
subItems = it.value.map { item ->
GradeDetailsItem(
grade = item,
valueBgColor = item.getBackgroundColor(gradeColorTheme),
weightString = weightString,
noDescriptionString = noDescriptionString
)
}
}
}
}
private fun formatAverage(average: Double): String {
private fun formatAverage(average: Double?): String {
return view?.run {
if (average == 0.0) emptyAverageString
if (average == null || average == .0) emptyAverageString
else averageString.format(average)
}.orEmpty()
}

View File

@ -1,17 +1,15 @@
package io.github.wulkanowy.ui.modules.grade.summary
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.repositories.grade.GradeRepository
import io.github.wulkanowy.data.repositories.gradessummary.GradeSummaryRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.session.BaseSessionPresenter
import io.github.wulkanowy.ui.base.session.SessionErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.changeModifier
import timber.log.Timber
import java.lang.String.format
import java.util.Locale.FRANCE
@ -20,10 +18,9 @@ import javax.inject.Inject
class GradeSummaryPresenter @Inject constructor(
private val errorHandler: SessionErrorHandler,
private val gradeSummaryRepository: GradeSummaryRepository,
private val gradeRepository: GradeRepository,
private val studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository,
private val preferencesRepository: PreferencesRepository,
private val averageProvider: GradeAverageProvider,
private val schedulers: SchedulersProvider,
private val analytics: FirebaseAnalyticsHelper
) : BaseSessionPresenter<GradeSummaryView>(errorHandler) {
@ -36,25 +33,13 @@ class GradeSummaryPresenter @Inject constructor(
fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) {
Timber.i("Loading grade summary data started")
disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getSemesters(it).map { semester -> semester to it } }
.map { pair -> pair.first.first { it.semesterId == semesterId } to pair.second }
.flatMap {
gradeSummaryRepository.getGradesSummary(it.first, forceRefresh)
.flatMap { semesterRepository.getSemesters(it).map { semesters -> it to semesters } }
.flatMap { (student, semesters) ->
gradeSummaryRepository.getGradesSummary(semesters.first { it.semesterId == semesterId }, forceRefresh)
.map { it.sortedBy { subject -> subject.subject } }
.flatMap { gradesSummary ->
gradeRepository.getGrades(it.second, it.first, forceRefresh)
.map { grades ->
grades.map { item -> item.changeModifier(preferencesRepository.gradePlusModifier, preferencesRepository.gradeMinusModifier) }
.groupBy { grade -> grade.subject }
.mapValues { entry -> entry.value.calcAverage() }
.filterValues { value -> value != 0.0 }
.let { averages ->
createGradeSummaryItems(gradesSummary, averages) to
GradeSummaryScrollableHeader(
formatAverage(gradesSummary.calcAverage()),
formatAverage(averages.values.average())
)
}
}
averageProvider.getGradeAverage(student, semesters, semesterId, forceRefresh)
.map { averages -> createGradeSummaryItemsAndHeader(gradesSummary, averages) }
}
}
.subscribeOn(schedulers.backgroundThread)
@ -66,14 +51,14 @@ class GradeSummaryPresenter @Inject constructor(
enableSwipe(true)
notifyParentDataLoaded(semesterId)
}
}.subscribe({
}.subscribe({ (gradeSummaryItems, gradeSummaryHeader) ->
Timber.i("Loading grade summary result: Success")
view?.run {
showEmpty(it.first.isEmpty())
showContent(it.first.isNotEmpty())
updateData(it.first, it.second)
showEmpty(gradeSummaryItems.isEmpty())
showContent(gradeSummaryItems.isNotEmpty())
updateData(gradeSummaryItems, gradeSummaryHeader)
}
analytics.logEvent("load_grade_summary", "items" to it.first.size, "force_refresh" to forceRefresh)
analytics.logEvent("load_grade_summary", "items" to gradeSummaryItems.size, "force_refresh" to forceRefresh)
}) {
Timber.i("Loading grade summary result: An exception occurred")
view?.run { showEmpty(isViewEmpty) }
@ -104,16 +89,24 @@ class GradeSummaryPresenter @Inject constructor(
disposable.clear()
}
private fun createGradeSummaryItems(gradesSummary: List<GradeSummary>, averages: Map<String, Double>)
: List<GradeSummaryItem> {
return gradesSummary.filter { !checkEmpty(it, averages) }.map { it ->
GradeSummaryItem(
title = it.subject,
average = formatAverage(averages.getOrElse(it.subject) { 0.0 }, ""),
predictedGrade = it.predictedGrade,
finalGrade = it.finalGrade
)
}
private fun createGradeSummaryItemsAndHeader(gradesSummary: List<GradeSummary>, averages: Map<String, Double>)
: Pair<List<GradeSummaryItem>, GradeSummaryScrollableHeader> {
return averages.filterValues { value -> value != 0.0 }
.let { filteredAverages ->
gradesSummary.filter { !checkEmpty(it, filteredAverages) }
.map {
GradeSummaryItem(
title = it.subject,
average = formatAverage(filteredAverages.getOrElse(it.subject) { 0.0 }, ""),
predictedGrade = it.predictedGrade,
finalGrade = it.finalGrade
)
}.let {
it to GradeSummaryScrollableHeader(
formatAverage(gradesSummary.calcAverage()),
formatAverage(filteredAverages.values.average()))
}
}
}
private fun checkEmpty(gradeSummary: GradeSummary, averages: Map<String, Double>): Boolean {

View File

@ -34,8 +34,8 @@ class HomeworkPresenter @Inject constructor(
fun onAttachView(view: HomeworkView, date: Long?) {
super.onAttachView(view)
Timber.i("Homework view is attached")
view.initView()
Timber.i("Homework view was initialized")
loadData(LocalDate.ofEpochDay(date ?: LocalDate.now().nextOrSameSchoolDay.toEpochDay()))
reloadView()
}

View File

@ -14,7 +14,7 @@ class LoginPresenter @Inject constructor(errorHandler: ErrorHandler) : BasePrese
initAdapter()
showActionBar(false)
}
Timber.i("Login view is attached")
Timber.i("Login view was initialized")
}
fun onFormViewAccountLogged(students: List<Student>, loginData: Triple<String, String, String>) {

View File

@ -16,6 +16,7 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.setOnItemSelectedListener
import io.github.wulkanowy.utils.setOnTextChangedListener
import io.github.wulkanowy.utils.showSoftInput
@ -53,7 +54,8 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
loginFormName.setOnTextChangedListener { presenter.onNameTextChanged() }
loginFormPass.setOnTextChangedListener { presenter.onPassTextChanged() }
loginFormHost.setOnItemSelectedListener { presenter.onHostSelected() }
loginFormSignIn.setOnClickListener { presenter.attemptLogin() }
loginFormSignIn.setOnClickListener { presenter.onSignInClick() }
loginFormPrivacyLink.setOnClickListener { presenter.onPrivacyLinkClick() }
loginFormPass.setOnEditorActionListener { _, id, _ ->
if (id == IME_ACTION_DONE || id == IME_NULL) loginFormSignIn.callOnClick() else false
@ -130,6 +132,10 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
}
}
override fun showPrivacyPolicy() {
loginFormPrivacyLink.visibility = VISIBLE
}
override fun notifyParentAccountLogged(students: List<Student>) {
(activity as? LoginActivity)?.onFormFragmentAccountLogged(students, Triple(
loginFormName.text.toString(),
@ -138,6 +144,10 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
))
}
override fun openPrivacyPolicyPage() {
context?.openInternetBrowser("https://wulkanowy.github.io/polityka-prywatnosci.html", ::showMessage)
}
override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView()

View File

@ -22,7 +22,8 @@ class LoginFormPresenter @Inject constructor(
super.onAttachView(view)
view.run {
initView()
if (isDebug) showVersion()
if (isDebug) showVersion() else showPrivacyPolicy()
errorHandler.onBadCredentials = {
setErrorPassIncorrect()
showSoftKeyboard()
@ -31,6 +32,10 @@ class LoginFormPresenter @Inject constructor(
}
}
fun onPrivacyLinkClick() {
view?.openPrivacyPolicyPage()
}
fun onHostSelected() {
view?.apply {
clearPassError()
@ -47,7 +52,7 @@ class LoginFormPresenter @Inject constructor(
view?.clearNameError()
}
fun attemptLogin() {
fun onSignInClick() {
val email = view?.formNameValue.orEmpty()
val password = view?.formPassValue.orEmpty()
val endpoint = view?.formHostValue.orEmpty()

View File

@ -37,5 +37,9 @@ interface LoginFormView : BaseView {
fun showVersion()
fun showPrivacyPolicy()
fun notifyParentAccountLogged(students: List<Student>)
fun openPrivacyPolicyPage()
}

View File

@ -1,14 +1,11 @@
package io.github.wulkanowy.ui.modules.login.studentselect
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
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 androidx.appcompat.app.AppCompatActivity
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -45,6 +42,7 @@ class LoginStudentSelectFragment : BaseFragment(), LoginStudentSelectView {
}
override fun initView() {
loginStudentSelectSignIn.setOnClickListener { presenter.onSignIn() }
loginAdapter.apply { setOnItemClickListener { presenter.onItemSelected(it) } }
loginStudentSelectRecycler.apply {
@ -54,14 +52,11 @@ class LoginStudentSelectFragment : BaseFragment(), LoginStudentSelectView {
}
override fun updateData(data: List<LoginStudentSelectItem>) {
loginAdapter.updateDataSet(data, true)
loginAdapter.updateDataSet(data)
}
override fun openMainView() {
activity?.let {
startActivity(MainActivity.getStartIntent(it)
.apply { addFlags(FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK) })
}
activity?.let { startActivity(MainActivity.getStartIntent(context = it, clear = true)) }
}
override fun showProgress(show: Boolean) {
@ -69,11 +64,11 @@ class LoginStudentSelectFragment : BaseFragment(), LoginStudentSelectView {
}
override fun showContent(show: Boolean) {
loginStudentSelectRecycler.visibility = if (show) VISIBLE else GONE
loginStudentSelectContent.visibility = if (show) VISIBLE else GONE
}
override fun showActionBar(show: Boolean) {
(activity as? AppCompatActivity)?.supportActionBar?.run { if (show) show() else hide() }
override fun enableSignIn(enable: Boolean) {
loginStudentSelectSignIn.isEnabled = enable
}
fun onParentInitStudentSelectFragment(students: List<Student>) {

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.login.studentselect
import android.annotation.SuppressLint
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -12,15 +13,16 @@ import kotlinx.android.synthetic.main.item_login_student_select.*
class LoginStudentSelectItem(val student: Student) : AbstractFlexibleItem<LoginStudentSelectItem.ItemViewHolder>() {
override fun getLayoutRes(): Int = R.layout.item_login_student_select
override fun getLayoutRes() = R.layout.item_login_student_select
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ItemViewHolder {
return ItemViewHolder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ItemViewHolder, position: Int, payloads: MutableList<Any>?) {
holder.run {
loginItemName.text = student.studentName
@SuppressLint("SetTextI18n")
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ItemViewHolder, position: Int, payloads: MutableList<Any>) {
holder.apply {
loginItemName.text = "${student.studentName} ${student.className}"
loginItemSchool.text = student.schoolName
}
}
@ -41,7 +43,17 @@ class LoginStudentSelectItem(val student: Student) : AbstractFlexibleItem<LoginS
}
class ItemViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
override val containerView: View
get() = itemView
init {
loginItemCheck.setOnClickListener { super.onClick(loginItemContainer) }
}
override fun onClick(view: View?) {
super.onClick(view)
loginItemCheck.apply { isChecked = !isChecked }
}
}
}

View File

@ -8,7 +8,6 @@ import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.reactivex.Single
import timber.log.Timber
import java.io.Serializable
import javax.inject.Inject
@ -22,10 +21,13 @@ class LoginStudentSelectPresenter @Inject constructor(
var students = emptyList<Student>()
var selectedStudents = mutableListOf<Student>()
fun onAttachView(view: LoginStudentSelectView, students: Serializable?) {
super.onAttachView(view)
view.run {
initView()
enableSignIn(false)
errorHandler.onStudentDuplicate = {
showMessage(it)
Timber.i("The student already registered in the app was selected")
@ -37,13 +39,21 @@ class LoginStudentSelectPresenter @Inject constructor(
}
}
fun onSignIn() {
registerStudents(selectedStudents)
}
fun onParentInitStudentSelectView(students: List<Student>) {
loadData(students)
if (students.size == 1) registerStudents(students)
}
fun onItemSelected(item: AbstractFlexibleItem<*>?) {
if (item is LoginStudentSelectItem) {
registerStudent(item.student)
selectedStudents.removeAll { it == item.student }
.let { if (!it) selectedStudents.add(item.student) }
view?.enableSignIn(selectedStudents.isNotEmpty())
}
}
@ -54,33 +64,30 @@ class LoginStudentSelectPresenter @Inject constructor(
}
}
private fun registerStudent(student: Student) {
disposable.add(studentRepository.saveStudent(student)
.map { student.apply { id = it } }
.onErrorResumeNext { studentRepository.logoutStudent(student).andThen(Single.error(it)) }
.flatMapCompletable { studentRepository.switchStudent(student) }
private fun registerStudents(students: List<Student>) {
disposable.add(studentRepository.saveStudents(students)
.map { students.first().apply { id = it.first() } }
.flatMapCompletable { studentRepository.switchStudent(it) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doOnSubscribe {
view?.apply {
showProgress(true)
showContent(false)
showActionBar(false)
}
Timber.i("Registration started")
}
.subscribe({
analytics.logEvent("registration_student_select", SUCCESS to true, "endpoint" to student.endpoint, "symbol" to student.symbol, "error" to "No error")
students.forEach { analytics.logEvent("registration_student_select", SUCCESS to true, "endpoint" to it.endpoint, "symbol" to it.symbol, "error" to "No error") }
Timber.i("Registration result: Success")
view?.openMainView()
}, {
analytics.logEvent("registration_student_select", SUCCESS to false, "endpoint" to student.endpoint, "symbol" to student.symbol, "error" to it.localizedMessage)
}, { error ->
students.forEach { analytics.logEvent("registration_student_select", SUCCESS to false, "endpoint" to it.endpoint, "symbol" to it.symbol, "error" to error.localizedMessage) }
Timber.i("Registration result: An exception occurred ")
errorHandler.dispatch(it)
errorHandler.dispatch(error)
view?.apply {
showProgress(false)
showContent(true)
showActionBar(true)
}
}))
}

View File

@ -14,5 +14,5 @@ interface LoginStudentSelectView : BaseView {
fun showContent(show: Boolean)
fun showActionBar(show: Boolean)
fun enableSignIn(enable: Boolean)
}

View File

@ -21,12 +21,12 @@ class LuckyNumberPresenter @Inject constructor(
override fun onAttachView(view: LuckyNumberView) {
super.onAttachView(view)
Timber.i("Lucky number view is attached")
view.run {
initView()
showContent(false)
enableSwipe(false)
}
Timber.i("Lucky number view was initialized")
loadData()
}

View File

@ -0,0 +1,77 @@
package io.github.wulkanowy.ui.modules.luckynumberwidget
import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.activity_widget_configure.*
import javax.inject.Inject
class LuckyNumberWidgetConfigureActivity : BaseActivity(), LuckyNumberWidgetConfigureView {
@Inject
lateinit var configureAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
@Inject
lateinit var presenter: LuckyNumberWidgetConfigurePresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setResult(RESULT_CANCELED)
setContentView(R.layout.activity_widget_configure)
intent.extras.let {
presenter.onAttachView(this, it?.getInt(EXTRA_APPWIDGET_ID))
}
}
override fun initView() {
widgetConfigureRecycler.apply {
adapter = configureAdapter
layoutManager = SmoothScrollLinearLayoutManager(context)
}
configureAdapter.setOnItemClickListener { presenter.onItemSelect(it) }
}
override fun updateData(data: List<LuckyNumberWidgetConfigureItem>) {
configureAdapter.updateDataSet(data)
}
override fun updateLuckyNumberWidget(widgetId: Int) {
sendBroadcast(Intent(this, LuckyNumberWidgetProvider::class.java)
.apply {
action = ACTION_APPWIDGET_UPDATE
putExtra(EXTRA_APPWIDGET_IDS, intArrayOf(widgetId))
})
}
override fun setSuccessResult(widgetId: Int) {
setResult(RESULT_OK, Intent().apply { putExtra(EXTRA_APPWIDGET_ID, widgetId) })
}
override fun showError(text: String, error: Throwable) {
Toast.makeText(this, text, Toast.LENGTH_LONG).show()
}
override fun finishView() {
finish()
}
override fun openLoginView() {
startActivity(LoginActivity.getStartIntent(this))
}
override fun onDestroy() {
super.onDestroy()
presenter.onDetachView()
}
}

View File

@ -0,0 +1,54 @@
package io.github.wulkanowy.ui.modules.luckynumberwidget
import android.annotation.SuppressLint
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetConfigureItem
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_account.*
class LuckyNumberWidgetConfigureItem(var student: Student, val isCurrent: Boolean) :
AbstractFlexibleItem<LuckyNumberWidgetConfigureItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_account
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter)
}
@SuppressLint("SetTextI18n")
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
holder.apply {
accountItemName.text = "${student.studentName} ${student.className}"
accountItemSchool.text = student.schoolName
accountItemImage.setBackgroundResource(if (isCurrent) R.drawable.ic_account_circular_border else 0)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TimetableWidgetConfigureItem
if (student != other.student) return false
return true
}
override fun hashCode(): Int {
var result = student.hashCode()
result = 31 * result + student.id.toInt()
return result
}
class ViewHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
override val containerView: View
get() = contentView
}
}

View File

@ -0,0 +1,62 @@
package io.github.wulkanowy.ui.modules.luckynumberwidget
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.db.SharedPrefHelper
import io.github.wulkanowy.data.db.entities.Student
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.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider.Companion.getStudentWidgetKey
import io.github.wulkanowy.utils.SchedulersProvider
import javax.inject.Inject
class LuckyNumberWidgetConfigurePresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val schedulers: SchedulersProvider,
private val studentRepository: StudentRepository,
private val sharedPref: SharedPrefHelper
) : BasePresenter<LuckyNumberWidgetConfigureView>(errorHandler) {
private var appWidgetId: Int? = null
fun onAttachView(view: LuckyNumberWidgetConfigureView, appWidgetId: Int?) {
super.onAttachView(view)
this.appWidgetId = appWidgetId
view.initView()
loadData()
}
fun onItemSelect(item: AbstractFlexibleItem<*>) {
if (item is LuckyNumberWidgetConfigureItem) {
registerStudent(item.student)
}
}
private fun loadData() {
disposable.add(studentRepository.getSavedStudents(false)
.map { it to appWidgetId?.let { id -> sharedPref.getLong(getStudentWidgetKey(id), 0) } }
.map { (students, currentStudentId) ->
students.map { student -> LuckyNumberWidgetConfigureItem(student, student.id == currentStudentId) }
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
when {
it.isEmpty() -> view?.openLoginView()
it.size == 1 -> registerStudent(it.single().student)
else -> view?.updateData(it)
}
}, { errorHandler.dispatch(it) }))
}
private fun registerStudent(student: Student) {
appWidgetId?.also {
sharedPref.putLong(getStudentWidgetKey(it), student.id)
view?.apply {
updateLuckyNumberWidget(it)
setSuccessResult(it)
}
}
view?.finishView()
}
}

View File

@ -0,0 +1,19 @@
package io.github.wulkanowy.ui.modules.luckynumberwidget
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetConfigureItem
interface LuckyNumberWidgetConfigureView : BaseView {
fun initView()
fun updateData(data: List<LuckyNumberWidgetConfigureItem>)
fun updateLuckyNumberWidget(widgetId: Int)
fun setSuccessResult(widgetId: Int)
fun finishView()
fun openLoginView()
}

View File

@ -0,0 +1,182 @@
package io.github.wulkanowy.ui.modules.luckynumberwidget
import android.annotation.TargetApi
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_DELETED
import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED
import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_OPTIONS
import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT
import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.RemoteViews
import dagger.android.AndroidInjection
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.SharedPrefHelper
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.repositories.luckynumber.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView.MenuView
import io.github.wulkanowy.utils.SchedulersProvider
import io.reactivex.Maybe
import timber.log.Timber
import javax.inject.Inject
class LuckyNumberWidgetProvider : BroadcastReceiver() {
@Inject
lateinit var studentRepository: StudentRepository
@Inject
lateinit var semesterRepository: SemesterRepository
@Inject
lateinit var luckyNumberRepository: LuckyNumberRepository
@Inject
lateinit var schedulers: SchedulersProvider
@Inject
lateinit var appWidgetManager: AppWidgetManager
@Inject
lateinit var sharedPref: SharedPrefHelper
companion object {
fun getStudentWidgetKey(appWidgetId: Int) = "lucky_number_widget_student_$appWidgetId"
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
override fun onReceive(context: Context, intent: Intent) {
AndroidInjection.inject(this, context)
when (intent.action) {
ACTION_APPWIDGET_UPDATE -> onUpdate(context, intent)
ACTION_APPWIDGET_DELETED -> onDelete(intent)
ACTION_APPWIDGET_OPTIONS_CHANGED -> onOptionsChange(context, intent)
}
}
private fun onUpdate(context: Context, intent: Intent) {
intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS)?.forEach { appWidgetId ->
RemoteViews(context.packageName, R.layout.widget_luckynumber).apply {
setTextViewText(R.id.luckyNumberWidgetNumber,
getLuckyNumber(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId)?.luckyNumber?.toString() ?: "#"
)
setOnClickPendingIntent(R.id.luckyNumberWidgetContainer,
PendingIntent.getActivity(context, MenuView.LUCKY_NUMBER.id,
MainActivity.getStartIntent(context, MenuView.LUCKY_NUMBER, true), FLAG_UPDATE_CURRENT))
}.also {
setStyles(it, intent)
appWidgetManager.updateAppWidget(appWidgetId, it)
}
}
}
private fun onDelete(intent: Intent) {
intent.getIntExtra(EXTRA_APPWIDGET_ID, 0).let {
if (it != 0) sharedPref.delete(getStudentWidgetKey(it))
}
}
private fun getLuckyNumber(studentId: Long, appWidgetId: Int): LuckyNumber? {
return try {
studentRepository.isStudentSaved()
.filter { true }
.flatMap { studentRepository.getSavedStudents().toMaybe() }
.flatMap { students ->
students.singleOrNull { student -> student.id == studentId }
.let { student ->
when {
student != null -> Maybe.just(student)
studentId != 0L -> {
studentRepository.getCurrentStudent(false)
.toMaybe()
.doOnSuccess { sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id) }
}
else -> null
}
}
}
.flatMap { semesterRepository.getCurrentSemester(it).toMaybe() }
.flatMap { luckyNumberRepository.getLuckyNumber(it) }
.subscribeOn(schedulers.backgroundThread)
.blockingGet()
} catch (e: Exception) {
Timber.e(e, "An error has occurred in lucky number provider")
null
}
}
private fun onOptionsChange(context: Context, intent: Intent) {
intent.extras?.let { extras ->
RemoteViews(context.packageName, R.layout.widget_luckynumber).apply {
setStyles(this, intent)
appWidgetManager.updateAppWidget(extras.getInt(EXTRA_APPWIDGET_ID), this)
}
}
}
@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")
views.run {
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")
views.run {
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")
views.run {
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")
views.run {
setViewVisibility(R.id.luckyNumberWidgetImageTop, GONE)
setViewVisibility(R.id.luckyNumberWidgetImageLeft, GONE)
setViewVisibility(R.id.luckyNumberWidgetTitle, VISIBLE)
setViewVisibility(R.id.luckyNumberWidgetNumber, VISIBLE)
}
}
}
}
}

View File

@ -5,7 +5,6 @@ import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.os.Bundle
import android.os.Handler
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
@ -22,8 +21,12 @@ import io.github.wulkanowy.ui.modules.account.AccountDialog
import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.safelyPopFragment
@ -40,9 +43,15 @@ class MainActivity : BaseActivity(), MainView {
lateinit var navController: FragNavController
companion object {
const val EXTRA_START_MENU_INDEX = "extraStartMenuIndex"
const val EXTRA_START_MENU = "extraStartMenu"
fun getStartIntent(context: Context) = Intent(context, MainActivity::class.java)
fun getStartIntent(context: Context, startMenu: MainView.MenuView? = null, clear: Boolean = false): Intent {
return Intent(context, MainActivity::class.java)
.apply {
if (clear) flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK
startMenu?.let { putExtra(EXTRA_START_MENU, it) }
}
}
}
override val isRootView: Boolean
@ -56,14 +65,27 @@ class MainActivity : BaseActivity(), MainView {
override var startMenuIndex = 0
override var startMenuMoreIndex = -1
private val moreMenuFragments = listOf<Fragment>(
MessageFragment.newInstance(),
HomeworkFragment.newInstance(),
NoteFragment.newInstance(),
LuckyNumberFragment.newInstance()
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(mainToolbar)
messageContainer = mainFragmentContainer
presenter.onAttachView(this, intent.getIntExtra(EXTRA_START_MENU_INDEX, -1))
navController.initialize(startMenuIndex, savedInstanceState)
presenter.onAttachView(this, intent.getSerializableExtra(EXTRA_START_MENU) as? MainView.MenuView)
navController.run {
initialize(startMenuIndex, savedInstanceState)
pushFragment(moreMenuFragments.getOrNull(startMenuMoreIndex))
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
@ -74,7 +96,7 @@ class MainActivity : BaseActivity(), MainView {
override fun initView() {
mainBottomNav.run {
addItems(
mutableListOf(
listOf(
AHBottomNavigationItem(R.string.grade_title, R.drawable.ic_menu_main_grade_26dp, 0),
AHBottomNavigationItem(R.string.attendance_title, R.drawable.ic_menu_main_attendance_24dp, 0),
AHBottomNavigationItem(R.string.exam_title, R.drawable.ic_menu_main_exam_24dp, 0),
@ -142,9 +164,7 @@ class MainActivity : BaseActivity(), MainView {
}
override fun notifyMenuViewReselected() {
Handler().postDelayed({
(navController.currentStack?.get(0) as? MainView.MainChildView)?.onFragmentReselected()
}, 250)
(navController.currentStack?.getOrNull(0) as? MainView.MainChildView)?.onFragmentReselected()
}
fun showDialogFragment(dialog: DialogFragment) {
@ -168,9 +188,10 @@ class MainActivity : BaseActivity(), MainView {
.apply { addFlags(FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK) })
}
override fun onSaveInstanceState(outState: Bundle?) {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
navController.onSaveInstanceState(outState)
intent.removeExtra(EXTRA_START_MENU)
}
override fun onDestroy() {

View File

@ -22,22 +22,19 @@ class MainPresenter @Inject constructor(
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<MainView>(errorHandler) {
fun onAttachView(view: MainView, initMenuIndex: Int) {
fun onAttachView(view: MainView, initMenu: MainView.MenuView?) {
super.onAttachView(view)
view.run {
startMenuIndex = if (initMenuIndex != -1) initMenuIndex else prefRepository.startMenuIndex
Timber.i("Main view is attached with $startMenuIndex menu index")
view.apply {
getProperViewIndexes(initMenu).let { (main, more) ->
startMenuIndex = main
startMenuMoreIndex = more
}
initView()
Timber.i("Main view was initialized with $startMenuIndex menu index and $startMenuMoreIndex more index")
}
syncManager.startSyncWorker()
analytics.logEvent(APP_OPEN, DESTINATION to when (initMenuIndex) {
1 -> "Grades"
3 -> "Timetable"
4 -> "More"
else -> "User action"
})
analytics.logEvent(APP_OPEN, DESTINATION to initMenu?.name)
}
fun onViewChange() {
@ -104,4 +101,12 @@ class MainPresenter @Inject constructor(
errorHandler.dispatch(it)
}))
}
private fun getProperViewIndexes(initMenu: MainView.MenuView?): Pair<Int, Int> {
return when {
initMenu?.id in 0..3 -> initMenu!!.id to -1
(initMenu?.id ?: 0) > 3 -> 4 to initMenu!!.id - 4
else -> prefRepository.startMenuIndex to -1
}
}
}

View File

@ -6,6 +6,8 @@ interface MainView : BaseView {
var startMenuIndex: Int
var startMenuMoreIndex: Int
val isRootView: Boolean
val currentViewTitle: String?
@ -37,4 +39,15 @@ interface MainView : BaseView {
val titleStringId: Int
}
enum class MenuView(val id: Int) {
GRADE(0),
ATTENDANCE(1),
EXAM(2),
TIMETABLE(3),
MESSAGE(4),
HOMEWORK(5),
NOTE(6),
LUCKY_NUMBER(7),
}
}

View File

@ -7,6 +7,7 @@ import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.ViewGroup
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.repositories.message.MessageFolder.RECEIVED
import io.github.wulkanowy.data.repositories.message.MessageFolder.SENT
import io.github.wulkanowy.data.repositories.message.MessageFolder.TRASHED
@ -75,12 +76,20 @@ class MessageFragment : BaseFragment(), MessageView, MainView.TitledView {
messageProgress.visibility = if (show) VISIBLE else INVISIBLE
}
fun onDeleteMessage(message: Message) {
presenter.onDeleteMessage(message)
}
fun onChildFragmentLoaded() {
presenter.onChildViewLoaded()
}
override fun notifyChildMessageDeleted(tabId: Int) {
(pagerAdapter.getFragmentInstance(tabId) as? MessageTabFragment)?.onParentDeleteMessage()
}
override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) {
(pagerAdapter.getFragmentInstance(index) as? MessageView.MessageChildView)?.onParentLoadData(forceRefresh)
(pagerAdapter.getFragmentInstance(index) as? MessageTabFragment)?.onParentLoadData(forceRefresh)
}
override fun openSendMessage() {

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.message
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.SchedulersProvider
@ -15,10 +16,10 @@ class MessagePresenter @Inject constructor(
override fun onAttachView(view: MessageView) {
super.onAttachView(view)
Timber.i("Message view is attached")
disposable.add(Completable.timer(150, MILLISECONDS, schedulers.mainThread)
.subscribe {
view.initView()
Timber.i("Message view was initialized")
loadData()
})
}
@ -43,6 +44,15 @@ class MessagePresenter @Inject constructor(
}
}
fun onDeleteMessage(message: Message) {
view?.notifyChildMessageDeleted(
when (message.removed) {
true -> 2
else -> message.folderId - 1
}
)
}
fun onSendMessageButtonClicked() {
view?.openSendMessage()
}

View File

@ -14,10 +14,7 @@ interface MessageView : BaseView {
fun notifyChildLoadData(index: Int, forceRefresh: Boolean)
fun notifyChildMessageDeleted(tabId: Int)
fun openSendMessage()
interface MessageChildView {
fun onParentLoadData(forceRefresh: Boolean)
}
}

View File

@ -13,7 +13,9 @@ import android.view.ViewGroup
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.ui.base.session.BaseSessionFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.send.SendMessageActivity
import kotlinx.android.synthetic.main.fragment_message_preview.*
import javax.inject.Inject
@ -25,6 +27,8 @@ class MessagePreviewFragment : BaseSessionFragment(), MessagePreviewView, MainVi
lateinit var presenter: MessagePreviewPresenter
private var menuReplyButton: MenuItem? = null
private var menuForwardButton: MenuItem? = null
private var menuDeleteButton: MenuItem? = null
override val titleStringId: Int
get() = R.string.message_title
@ -32,6 +36,9 @@ class MessagePreviewFragment : BaseSessionFragment(), MessagePreviewView, MainVi
override val noSubjectString: String
get() = getString(R.string.message_no_subject)
override val deleteMessageSuccessString: String
get() = getString(R.string.message_delete_success)
companion object {
const val MESSAGE_ID_KEY = "message_id"
@ -57,15 +64,21 @@ class MessagePreviewFragment : BaseSessionFragment(), MessagePreviewView, MainVi
presenter.onAttachView(this, (savedInstanceState ?: arguments)?.getInt(MESSAGE_ID_KEY) ?: 0)
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.action_menu_message_preview, menu)
menuReplyButton = menu?.findItem(R.id.messagePreviewMenuReply)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.action_menu_message_preview, menu)
menuReplyButton = menu.findItem(R.id.messagePreviewMenuReply)
menuForwardButton = menu.findItem(R.id.messagePreviewMenuForward)
menuDeleteButton = menu.findItem(R.id.messagePreviewMenuDelete)
presenter.onCreateOptionsMenu()
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
return if (item?.itemId == R.id.messagePreviewMenuReply) presenter.onReply()
else false
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.messagePreviewMenuReply -> presenter.onReply()
R.id.messagePreviewMenuForward -> presenter.onForward()
R.id.messagePreviewMenuDelete -> presenter.onMessageDelete()
else -> false
}
}
override fun setSubject(subject: String) {
@ -92,8 +105,22 @@ class MessagePreviewFragment : BaseSessionFragment(), MessagePreviewView, MainVi
messagePreviewProgress.visibility = if (show) VISIBLE else GONE
}
override fun showReplyButton(show: Boolean) {
override fun showContent(show: Boolean) {
messagePreviewContentContainer.visibility = if (show) VISIBLE else GONE
}
override fun showOptions(show: Boolean) {
menuReplyButton?.isVisible = show
menuForwardButton?.isVisible = show
menuDeleteButton?.isVisible = show
}
override fun setDeletedOptionsLabels() {
menuDeleteButton?.setTitle(R.string.message_delete_forever)
}
override fun setNotDeletedOptionsLabels() {
menuDeleteButton?.setTitle(R.string.message_move_to_bin)
}
override fun showMessageError() {
@ -101,9 +128,21 @@ class MessagePreviewFragment : BaseSessionFragment(), MessagePreviewView, MainVi
}
override fun openMessageReply(message: Message?) {
context?.let { it.startActivity(SendMessageActivity.getStartIntent(it, message, true)) }
}
override fun openMessageForward(message: Message?) {
context?.let { it.startActivity(SendMessageActivity.getStartIntent(it, message)) }
}
override fun popView() {
(activity as MainActivity).popView()
}
override fun notifyParentMessageDeleted(message: Message) {
fragmentManager?.fragments?.forEach { if (it is MessageFragment) it.onDeleteMessage(message) }
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(MESSAGE_ID_KEY, presenter.messageId)

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