Services refactor (#168)

This commit is contained in:
Mikołaj Pich 2018-11-01 19:27:02 +01:00 committed by Rafał Borcz
parent ab71dd3fde
commit 70879945f2
52 changed files with 899 additions and 188 deletions

View File

@ -38,7 +38,7 @@ jobs:
command: ./gradlew build -x test -x lint -x fabricGenerateResourcesRelease -x packageRelease --no-daemon --stacktrace --console=plain -PdisablePreDex
- run:
name: Run FOSSA
command: fossa --no-ansi
command: fossa --no-ansi || true
- persist_to_workspace:
root: *workspace_root
paths:
@ -113,7 +113,7 @@ jobs:
adb shell input keyevent 82
- run:
name: Run instrumented tests
command: ./gradlew clean createDebugCoverageReport jacocoTestReport --no-daemon --stacktrace --console=plain -PdisablePreDex
command: ./gradlew clean createDebugCoverageReport jacocoTestReport --no-daemon --stacktrace --console=plain -PdisablePreDex -PdisableCrashlytics
- run:
name: Collect logs from emulator
command: adb logcat -d > ./app/build/reports/logcat_emulator.txt

3
.gitignore vendored
View File

@ -33,7 +33,6 @@ local.properties
.idea/vcs.xml
.idea/workspace.xml
.idea/caches/
.idea/codeStyles/
*.iml
# OS-specific files
@ -44,7 +43,7 @@ local.properties
.Trashes
ehthumbs.db
Thumbs.db
.idea/codeStyles/
.idea/caches/
./app/key.p12
./app/upload-key.jks
*.log

View File

@ -0,0 +1,174 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#10;" />
<AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" />
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" withSubpackages="true" static="false" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CONTINUATION_INDENT_IN_PARAMETER_LISTS" value="false" />
<option name="CONTINUATION_INDENT_IN_ARGUMENT_LISTS" value="false" />
<option name="CONTINUATION_INDENT_FOR_EXPRESSION_BODIES" value="false" />
<option name="CONTINUATION_INDENT_FOR_CHAINED_CALLS" value="false" />
<option name="CONTINUATION_INDENT_IN_SUPERTYPE_LISTS" value="false" />
<option name="CONTINUATION_INDENT_IN_IF_CONDITIONS" value="false" />
<option name="CONTINUATION_INDENT_IN_ELVIS" value="false" />
<option name="WRAP_EXPRESSION_BODY_FUNCTIONS" value="1" />
</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" />
<option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</XML>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="METHOD_PARAMETERS_WRAP" value="5" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="ASSIGNMENT_WRAP" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@ -7,6 +7,8 @@ apply plugin: 'com.github.triplet.play'
apply from: 'jacoco.gradle'
apply from: 'sonarqube.gradle'
def fabricApiKey = System.getenv("FABRIC_API_KEY") ?: "null"
android {
compileSdkVersion 28
buildToolsVersion '28.0.3'
@ -29,9 +31,7 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
playAccountConfig = playAccountConfigs.defaultAccountConfig
manifestPlaceholders = [
fabricApiKey: System.getenv("FABRIC_API_KEY") ?: "null"
]
manifestPlaceholders = [ fabricApiKey: fabricApiKey ]
}
signingConfigs {
@ -45,18 +45,24 @@ android {
buildTypes {
release {
buildConfigField "boolean", "FABRIC_ENABLED", "true"
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
debug {
buildConfigField "boolean", "FABRIC_ENABLED", fabricApiKey == "null" ? "false" : "true"
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
testCoverageEnabled = true
ext.enableCrashlytics = false
ext.enableCrashlytics = fabricApiKey != "null" && !project.hasProperty("disableCrashlytics")
multiDexKeepProguard file('proguard-multidex-rules.pro')
}
}
lintOptions {
disable 'HardwareIds'
}
}
androidExtensions {
@ -72,7 +78,7 @@ ext.androidx_version = "1.0.0"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation('com.github.wulkanowy:api:1400211f0d') { exclude module: "threetenbp" }
implementation('com.github.wulkanowy:api:e829b094de') { exclude module: "threetenbp" }
implementation "androidx.legacy:legacy-support-v4:$androidx_version"
implementation "androidx.appcompat:appcompat:$androidx_version"
@ -89,12 +95,13 @@ dependencies {
kapt "com.google.dagger:dagger-compiler:2.16"
kapt "com.google.dagger:dagger-android-processor:2.16"
implementation "androidx.room:room-runtime:2.1.0-alpha01"
implementation "androidx.room:room-rxjava2:2.1.0-alpha01"
kapt "androidx.room:room-compiler:2.1.0-alpha01"
implementation "androidx.room:room-runtime:2.1.0-alpha02"
implementation "androidx.room:room-rxjava2:2.1.0-alpha02"
kapt "androidx.room:room-compiler:2.1.0-alpha02"
implementation "eu.davidea:flexible-adapter:5.1.0"
implementation "eu.davidea:flexible-adapter-ui:1.0.0"
implementation "com.aurelhubert:ahbottomnavigation:2.2.0"
implementation 'com.ncapdevi:frag-nav:3.0.0-RC3'
@ -120,9 +127,10 @@ dependencies {
testImplementation "org.mockito:mockito-inline:2.23.0"
testImplementation 'org.threeten:threetenbp:1.3.7'
androidTestImplementation 'androidx.test:core:1.0.0-beta02'
androidTestImplementation 'androidx.test:runner:1.1.0-beta02'
androidTestImplementation 'androidx.test.ext:junit:1.0.0-beta02'
androidTestImplementation 'androidx.test:core:1.0.0'
androidTestImplementation 'androidx.test:runner:1.1.0'
androidTestImplementation 'androidx.test.ext:junit:1.0.0'
androidTestImplementation "org.mockito:mockito-android:2.23.0"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
}

View File

@ -40,6 +40,14 @@
android:launchMode="singleTop"
android:theme="@style/WulkanowyTheme.NoActionBar" />
<service
android:name=".services.job.SyncWorker"
android:exported="false">
<intent-filter>
<action android:name="com.firebase.jobdispatcher.ACTION_EXECUTE" />
</intent-filter>
</service>
<meta-data
android:name="io.fabric.ApiKey"
android:value="${fabricApiKey}" />

View File

@ -39,7 +39,7 @@ class WulkanowyApp : DaggerApplication() {
private fun initializeFabric() {
Fabric.with(Fabric.Builder(this)
.kits(Crashlytics.Builder()
.core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build())
.core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG || !BuildConfig.FABRIC_ENABLED).build())
.build(),
Answers())
.debuggable(BuildConfig.DEBUG)

View File

@ -14,7 +14,7 @@ open class ErrorHandler @Inject constructor(private val resources: Resources) {
var showErrorMessage: (String) -> Unit = {}
open fun proceed(error: Throwable) {
Timber.i(error, "An exception occurred while the Wulkanowy was running")
Timber.e(error, "An exception occurred while the Wulkanowy was running")
showErrorMessage((when (error) {
is UnknownHostException -> resources.getString(R.string.all_no_internet)

View File

@ -14,4 +14,12 @@ class SharedPrefHelper @Inject constructor(private val sharedPref: SharedPrefere
fun getLong(key: String, defaultValue: Long): Long {
return sharedPref.getLong(key, defaultValue)
}
}
fun getBoolean(key: String, defaultValue: Boolean): Boolean {
return sharedPref.getBoolean(key, defaultValue)
}
fun getString(key: String, defaultValue: String): String {
return sharedPref.getString(key, defaultValue) ?: defaultValue
}
}

View File

@ -13,9 +13,15 @@ interface GradeDao {
@Update
fun update(grade: Grade)
@Update
fun updateAll(grade: List<Grade>)
@Delete
fun deleteAll(grades: List<Grade>)
@Query("SELECT * FROM Grades WHERE semester_id = :semesterId AND student_id = :studentId")
fun getGrades(semesterId: Int, studentId: Int): Maybe<List<Grade>>
@Query("SELECT * FROM Grades WHERE is_read = 0 AND semester_id = :semesterId AND student_id = :studentId")
fun getNewGrades(semesterId: Int, studentId: Int): Maybe<List<Grade>>
}

View File

@ -11,7 +11,7 @@ import io.reactivex.Maybe
@Dao
interface GradeSummaryDao {
@Insert(onConflict = REPLACE)
@Insert
fun insertAll(gradesSummary: List<GradeSummary>)
@Delete

View File

@ -14,4 +14,10 @@ interface SemesterDao {
@Query("SELECT * FROM Semesters WHERE student_id = :studentId")
fun getSemester(studentId: Int): Single<List<Semester>>
@Query("UPDATE Semesters SET is_current = 0")
fun resetCurrentSemester()
@Query("UPDATE Semesters SET is_current = 1 WHERE semester_id = :semesterId")
fun setCurrentSemester(semesterId: Int)
}

View File

@ -9,41 +9,44 @@ import java.io.Serializable
@Entity(tableName = "Grades")
data class Grade(
@ColumnInfo(name = "semester_id")
var semesterId: Int,
@ColumnInfo(name = "semester_id")
var semesterId: Int,
@ColumnInfo(name = "student_id")
var studentId: Int,
@ColumnInfo(name = "student_id")
var studentId: Int,
var subject: String,
var subject: String,
var entry: String,
var entry: String,
var value: Int,
var value: Int,
var modifier: Double,
var modifier: Double,
var comment: String,
var comment: String,
var color: String,
var color: String,
@ColumnInfo(name = "grade_symbol")
var gradeSymbol: String,
@ColumnInfo(name = "grade_symbol")
var gradeSymbol: String,
var description: String,
var description: String,
var weight: String,
var weight: String,
var weightValue: Int,
var weightValue: Int,
var date: LocalDate,
var date: LocalDate,
var teacher: String
var teacher: String
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
@ColumnInfo(name = "is_new")
var isNew: Boolean = false
@ColumnInfo(name = "is_read")
var isRead: Boolean = true
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
}

View File

@ -14,29 +14,39 @@ import javax.inject.Singleton
@Singleton
class GradeRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: GradeLocal,
private val remote: GradeRemote
private val settings: InternetObservingSettings,
private val local: GradeLocal,
private val remote: GradeRemote
) {
fun getGrades(semester: Semester, forceRefresh: Boolean = false): Single<List<Grade>> {
fun getGrades(semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Single<List<Grade>> {
return local.getGrades(semester).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getGrades(semester)
else Single.error(UnknownHostException())
}.flatMap { newGrades ->
local.getGrades(semester).toSingle(emptyList())
.doOnSuccess { oldGrades ->
local.deleteGrades(oldGrades - newGrades)
local.saveGrades((newGrades - oldGrades)
.onEach { if (oldGrades.isNotEmpty()) it.isNew = true })
}
}.flatMap { local.getGrades(semester).toSingle(emptyList()) })
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getGrades(semester)
else Single.error(UnknownHostException())
}.flatMap { newGrades ->
local.getGrades(semester).toSingle(emptyList())
.doOnSuccess { oldGrades ->
local.deleteGrades(oldGrades - newGrades)
local.saveGrades((newGrades - oldGrades)
.onEach {
if (oldGrades.isNotEmpty()) it.isRead = false
if (notify) it.isNotified = false
})
}
}.flatMap { local.getGrades(semester).toSingle(emptyList()) })
}
fun getNewGrades(semester: Semester): Single<List<Grade>> {
return local.getNewGrades(semester).toSingle(emptyList())
}
fun updateGrade(grade: Grade): Completable {
return local.updateGrade(grade)
}
fun updateGrades(grades: List<Grade>): Completable {
return local.updateGrades(grades)
}
}

View File

@ -1,19 +1,39 @@
package io.github.wulkanowy.data.repositories
import android.content.Context
import android.content.SharedPreferences
import io.github.wulkanowy.R
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PreferencesRepository @Inject constructor(private val sharedPref: SharedPreferences) {
class PreferencesRepository @Inject constructor(
private val sharedPref: SharedPreferences,
val context: Context
) {
val startMenuIndex: Int
get() = sharedPref.getString("start_menu", "0")?.toInt() ?: 0
get() = sharedPref.getString(context.getString(R.string.pref_key_start_menu), "0")?.toInt() ?: 0
val showPresent: Boolean
get() = sharedPref.getBoolean("attendance_present", true)
get() = sharedPref.getBoolean(context.getString(R.string.pref_key_attendance_present), true)
val currentThemeKey: String = context.getString(R.string.pref_key_theme)
val currentTheme: Int
get() = sharedPref.getString("theme", "1")?.toInt() ?: 1
}
get() = sharedPref.getString(currentThemeKey, "1")?.toInt() ?: 1
val serviceEnablesKey: String = context.getString(R.string.pref_key_services_enable)
val serviceEnabled: Boolean
get() = sharedPref.getBoolean(serviceEnablesKey, true)
val servicesIntervalKey: String = context.getString(R.string.pref_key_services_interval)
val servicesInterval: Int
get() = sharedPref.getString(servicesIntervalKey, "60")?.toInt() ?: 60
val servicesOnlyWifiKey: String = context.getString(R.string.pref_key_services_wifi_only)
val servicesOnlyWifi: Boolean
get() = sharedPref.getBoolean(servicesOnlyWifiKey, true)
val notificationsEnable: Boolean
get() = sharedPref.getBoolean(context.getString(R.string.pref_key_notifications_enable), true)
}

View File

@ -14,9 +14,10 @@ import javax.inject.Singleton
@Singleton
class SessionRepository @Inject constructor(
private val local: SessionLocal,
private val remote: SessionRemote,
private val settings: InternetObservingSettings) {
private val local: SessionLocal,
private val remote: SessionRemote,
private val settings: InternetObservingSettings
) {
val isSessionSaved
get() = local.isSessionSaved
@ -26,24 +27,39 @@ class SessionRepository @Inject constructor(
fun getConnectedStudents(email: String, password: String, symbol: String, endpoint: String): Single<List<Student>> {
cachedStudents = ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap { isConnected ->
if (isConnected) remote.getConnectedStudents(email, password, symbol, endpoint)
else Single.error<List<Student>>(UnknownHostException("No internet connection"))
}.doOnSuccess { cachedStudents = Single.just(it) }
.flatMap { isConnected ->
if (isConnected) remote.getConnectedStudents(email, password, symbol, endpoint)
else Single.error<List<Student>>(UnknownHostException("No internet connection"))
}.doOnSuccess { cachedStudents = Single.just(it) }
return cachedStudents
}
fun getSemesters(): Single<List<Semester>> {
return local.getLastStudent()
.flatMapSingle {
remote.initApi(it, true)
local.getSemesters(it)
}
fun saveStudent(student: Student): Completable {
return remote.getSemesters(student)
.flatMapCompletable { local.saveSemesters(it) }
.concatWith(local.saveStudent(student))
}
fun saveStudent(student: Student): Completable {
return remote.getSemesters(student).flatMapCompletable { local.saveSemesters(it) }
.concatWith(local.saveStudent(student))
fun getSemesters(forceRefresh: Boolean = false): Single<List<Semester>> {
return local.getLastStudent()
.flatMapSingle { student ->
remote.initApi(student)
local.getSemesters(student).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings).flatMap {
if (it) remote.getCurrentSemester(student)
else Single.error(UnknownHostException())
}.flatMap { current ->
local.getSemesters(student).doOnSuccess { semesters ->
if (semesters.single { it.current }.semesterId != current.semesterId) {
local.saveSemesters(listOf(current)).andThen {
local.setCurrentSemester(current.semesterId)
}
}
}
}.flatMap {
local.getSemesters(student)
})
}
}
fun clearCache() {

View File

@ -15,6 +15,10 @@ class GradeLocal @Inject constructor(private val gradeDb: GradeDao) {
return gradeDb.getGrades(semester.semesterId, semester.studentId).filter { !it.isEmpty() }
}
fun getNewGrades(semester: Semester): Maybe<List<Grade>> {
return gradeDb.getNewGrades(semester.semesterId, semester.studentId)
}
fun saveGrades(grades: List<Grade>) {
gradeDb.insertAll(grades)
}
@ -23,6 +27,10 @@ class GradeLocal @Inject constructor(private val gradeDb: GradeDao) {
return Completable.fromCallable { gradeDb.update(grade) }
}
fun updateGrades(grade: List<Grade>): Completable {
return Completable.fromCallable { gradeDb.updateAll(grade) }
}
fun deleteGrades(grades: List<Grade>) {
gradeDb.deleteAll(grades)
}

View File

@ -46,4 +46,10 @@ class SessionLocal @Inject constructor(
fun getSemesters(student: Student): Single<List<Semester>> {
return semesterDb.getSemester(student.studentId)
}
fun setCurrentSemester(semesterId: Int): Completable {
return Single.fromCallable { semesterDb.resetCurrentSemester() }.ignoreElement().andThen {
semesterDb.setCurrentSemester(semesterId)
}
}
}

View File

@ -12,37 +12,46 @@ import javax.inject.Singleton
class SessionRemote @Inject constructor(private val api: Api) {
fun getConnectedStudents(email: String, password: String, symbol: String, endpoint: String): Single<List<Student>> {
return Single.just(initApi(Student(email = email, password = password, symbol = symbol, endpoint = endpoint, loginType = "AUTO")))
.flatMap { _ ->
api.getPupils().map { students ->
students.map {
Student(
email = email,
password = password,
symbol = it.symbol,
studentId = it.studentId,
studentName = it.studentName,
schoolSymbol = it.schoolSymbol,
schoolName = it.schoolName,
endpoint = endpoint,
loginType = it.loginType.name
)
}
}
return Single.just(
initApi(
Student(
email = email,
password = password,
symbol = symbol,
endpoint = endpoint,
loginType = "AUTO"
), true
)
).flatMap {
api.getPupils().map { students ->
students.map { pupil ->
Student(
email = email,
password = password,
symbol = pupil.symbol,
studentId = pupil.studentId,
studentName = pupil.studentName,
schoolSymbol = pupil.schoolSymbol,
schoolName = pupil.schoolName,
endpoint = endpoint,
loginType = pupil.loginType.name
)
}
}
}
}
fun getSemesters(student: Student): Single<List<Semester>> {
return Single.just(initApi(student)).flatMap { _ ->
return Single.just(initApi(student)).flatMap {
api.getSemesters().map { semesters ->
semesters.map {
semesters.map { semester ->
Semester(
studentId = student.studentId,
diaryId = it.diaryId,
diaryName = it.diaryName,
semesterId = it.semesterId,
semesterName = it.semesterNumber,
current = it.current
studentId = student.studentId,
diaryId = semester.diaryId,
diaryName = semester.diaryName,
semesterId = semester.semesterId,
semesterName = semester.semesterNumber,
current = semester.current
)
}
@ -50,8 +59,21 @@ class SessionRemote @Inject constructor(private val api: Api) {
}
}
fun initApi(student: Student, checkInit: Boolean = false) {
if (if (checkInit) 0 == api.studentId else true) {
fun getCurrentSemester(student: Student): Single<Semester> {
return api.getCurrentSemester().map {
Semester(
studentId = student.studentId,
diaryId = it.diaryId,
diaryName = it.diaryName,
semesterId = it.semesterId,
semesterName = it.semesterNumber,
current = it.current
)
}
}
fun initApi(student: Student, reInitialize: Boolean = false) {
if (if (reInitialize) true else 0 == api.studentId) {
api.run {
email = student.email
password = student.password

View File

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

View File

@ -1,6 +1,8 @@
package io.github.wulkanowy.di
import android.content.Context
import com.firebase.jobdispatcher.FirebaseJobDispatcher
import com.firebase.jobdispatcher.GooglePlayDriver
import dagger.Module
import dagger.Provides
import eu.davidea.flexibleadapter.FlexibleAdapter
@ -22,4 +24,10 @@ internal class AppModule {
@Provides
fun provideFlexibleAdapter() = FlexibleAdapter<AbstractFlexibleItem<*>>(null, null, true)
@Singleton
@Provides
fun provideJobDispatcher(context: Context): FirebaseJobDispatcher {
return FirebaseJobDispatcher(GooglePlayDriver(context))
}
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.di
import dagger.Module
import dagger.android.ContributesAndroidInjector
import io.github.wulkanowy.di.scopes.PerActivity
import io.github.wulkanowy.services.job.SyncWorker
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginModule
import io.github.wulkanowy.ui.modules.main.MainActivity
@ -23,4 +24,7 @@ internal abstract class BuilderModule {
@PerActivity
@ContributesAndroidInjector(modules = [MainModule::class])
abstract fun bindMainActivity(): MainActivity
@ContributesAndroidInjector
abstract fun bindSyncJob(): SyncWorker
}

View File

@ -0,0 +1,57 @@
package io.github.wulkanowy.services.job
import com.firebase.jobdispatcher.Constraint.ON_ANY_NETWORK
import com.firebase.jobdispatcher.Constraint.ON_UNMETERED_NETWORK
import com.firebase.jobdispatcher.FirebaseJobDispatcher
import com.firebase.jobdispatcher.Lifetime.FOREVER
import com.firebase.jobdispatcher.RetryStrategy.DEFAULT_EXPONENTIAL
import com.firebase.jobdispatcher.Trigger.executionWindow
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.utils.isHolidays
import org.threeten.bp.LocalDate
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ServiceHelper @Inject constructor(
private val prefRepository: PreferencesRepository,
private val dispatcher: FirebaseJobDispatcher
) {
fun reloadFullSyncService() {
startFullSyncService(true)
}
fun startFullSyncService(replaceCurrent: Boolean = false) {
if (LocalDate.now().isHolidays || !prefRepository.serviceEnabled) {
Timber.d("Services disabled or it's holidays")
return
}
dispatcher.mustSchedule(
dispatcher.newJobBuilder()
.setLifetime(FOREVER)
.setRecurring(true)
.setService(SyncWorker::class.java)
.setTag(SyncWorker.WORK_TAG)
.setTrigger(
executionWindow(
prefRepository.servicesInterval * 60,
(prefRepository.servicesInterval + 10) * 60
)
)
.setConstraints(if (prefRepository.servicesOnlyWifi) ON_UNMETERED_NETWORK else ON_ANY_NETWORK)
.setReplaceCurrent(replaceCurrent)
.setRetryStrategy(DEFAULT_EXPONENTIAL)
.build()
)
Timber.d("Services started")
}
fun stopFullSyncService() {
dispatcher.cancel(SyncWorker.WORK_TAG)
Timber.d("Services stopped")
}
}

View File

@ -0,0 +1,110 @@
package io.github.wulkanowy.services.job
import com.firebase.jobdispatcher.JobParameters
import com.firebase.jobdispatcher.SimpleJobService
import dagger.android.AndroidInjection
import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.ExamRepository
import io.github.wulkanowy.data.repositories.GradeRepository
import io.github.wulkanowy.data.repositories.GradeSummaryRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.services.notification.GradeNotification
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.monday
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import org.threeten.bp.LocalDate
import timber.log.Timber
import javax.inject.Inject
class SyncWorker : SimpleJobService() {
@Inject
lateinit var session: SessionRepository
@Inject
lateinit var gradesDetails: GradeRepository
@Inject
lateinit var gradesSummary: GradeSummaryRepository
@Inject
lateinit var attendance: AttendanceRepository
@Inject
lateinit var exam: ExamRepository
@Inject
lateinit var timetable: TimetableRepository
@Inject
lateinit var prefRepository: PreferencesRepository
private val disposable = CompositeDisposable()
companion object {
const val WORK_TAG = "FULL_SYNC"
}
override fun onCreate() {
super.onCreate()
AndroidInjection.inject(this)
}
override fun onRunJob(job: JobParameters?): Int {
Timber.d("Synchronization started")
val start = LocalDate.now().monday
val end = LocalDate.now().friday
if (start.isHolidays) return RESULT_FAIL_NORETRY
var error: Throwable? = null
disposable.add(session.getSemesters(true)
.map { it.single { semester -> semester.current } }
.flatMapPublisher {
Single.merge(
listOf(
gradesDetails.getGrades(it, true, true),
gradesSummary.getGradesSummary(it, true),
attendance.getAttendance(it, start, end, true),
exam.getExams(it, start, end, true),
timetable.getTimetable(it, start, end, true)
)
)
}
.subscribe({}, { error = it }))
return if (null === error) {
if (prefRepository.notificationsEnable) sendNotifications()
Timber.d("Synchronization successful")
RESULT_SUCCESS
} else {
Timber.e(error, "Synchronization failed")
RESULT_FAIL_RETRY
}
}
private fun sendNotifications() {
disposable.add(session.getSemesters(true)
.map { it.single { semester -> semester.current } }
.flatMap { gradesDetails.getNewGrades(it) }
.map { it.filter { grade -> !grade.isNotified } }
.subscribe({
if (it.isNotEmpty()) {
Timber.d("Found ${it.size} unread grades")
GradeNotification(applicationContext).sendNotification(it)
gradesDetails.updateGrades(it.map { grade -> grade.apply { isNotified = true } }).subscribe()
}
}) { Timber.e("Notifications sending failed") })
}
override fun onDestroy() {
super.onDestroy()
disposable.clear()
}
}

View File

@ -0,0 +1,34 @@
package io.github.wulkanowy.services.notification
import android.app.Notification
import android.app.NotificationManager
import android.content.Context
import android.content.Context.NOTIFICATION_SERVICE
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O
import androidx.core.app.NotificationCompat
import timber.log.Timber
import kotlin.random.Random
abstract class BaseNotification(protected val context: Context) {
protected val notificationManager: NotificationManager by lazy {
context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
}
fun notify(notification: Notification) {
notificationManager.notify(Random.nextInt(1000), notification)
}
fun notificationBuilder(channelId: String): NotificationCompat.Builder {
if (SDK_INT >= O) createChannel(channelId)
return NotificationCompat.Builder(context, channelId)
}
fun cancelAll() {
notificationManager.cancelAll()
Timber.d("Notifications canceled")
}
abstract fun createChannel(channelId: String)
}

View File

@ -0,0 +1,60 @@
package io.github.wulkanowy.services.notification
import android.annotation.TargetApi
import android.app.Notification.VISIBILITY_PUBLIC
import android.app.NotificationChannel
import android.app.NotificationManager.IMPORTANCE_HIGH
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainActivity.Companion.EXTRA_CARD_ID_KEY
import timber.log.Timber
class GradeNotification(context: Context) : BaseNotification(context) {
private val channelId = "Grade_Notify"
@TargetApi(26)
override fun createChannel(channelId: String) {
notificationManager.createNotificationChannel(NotificationChannel(
channelId, context.getString(R.string.notify_grade_channel), IMPORTANCE_HIGH
).apply {
enableLights(true)
enableVibration(true)
lockscreenVisibility = VISIBILITY_PUBLIC
})
}
fun sendNotification(items: List<Grade>) {
notify(notificationBuilder(channelId)
.setContentTitle(context.resources.getQuantityString(R.plurals.grade_new_items, items.size, items.size))
.setContentText(context.resources.getQuantityString(R.plurals.notify_grade_new_items, items.size, items.size))
.setSmallIcon(R.drawable.ic_stat_notify_grade)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(ContextCompat.getColor(context, R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(context, 0,
MainActivity.getStartIntent(context).putExtra(EXTRA_CARD_ID_KEY, 0),
FLAG_UPDATE_CURRENT
)
)
.setStyle(NotificationCompat.InboxStyle().run {
setSummaryText(context.resources.getQuantityString(R.plurals.grade_number_item, items.size, items.size))
items.forEach {
addLine("${it.subject}: ${it.entry}")
}
this
})
.build()
)
Timber.d("Notification sent")
}
}

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.base
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.ui.modules.main.MainView
import io.reactivex.disposables.CompositeDisposable
open class BasePresenter<T : BaseView>(private val errorHandler: ErrorHandler) {

View File

@ -12,4 +12,3 @@ class AboutModule {
@Provides
fun provideLibsFragmentCompat() = LibsFragmentCompat()
}

View File

@ -7,4 +7,4 @@ interface AboutView : BaseView {
fun openSourceWebView()
fun openIssuesWebView()
}
}

View File

@ -29,4 +29,3 @@ abstract class GradeModule {
@ContributesAndroidInjector
abstract fun binGradeSummaryFragment(): GradeSummaryFragment
}

View File

@ -75,6 +75,4 @@ class GradeDetailsDialog : DialogFragment() {
gradeDialogClose.setOnClickListener { dismiss() }
}
}

View File

@ -34,7 +34,7 @@ class GradeDetailsItem(val grade: Grade, private val weightString: String, priva
gradeItemDescription.text = if (grade.description.isNotBlank()) grade.description else grade.gradeSymbol
gradeItemDate.text = grade.date.toFormattedString()
gradeItemWeight.text = "$weightString: ${grade.weight}"
gradeItemNote.visibility = if (grade.isNew) VISIBLE else GONE
gradeItemNote.visibility = if (!grade.isRead) VISIBLE else GONE
}
}

View File

@ -9,6 +9,7 @@ import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.calcAverage
import io.github.wulkanowy.utils.valueColor
import timber.log.Timber
import javax.inject.Inject
class GradeDetailsPresenter @Inject constructor(
@ -51,8 +52,8 @@ class GradeDetailsPresenter @Inject constructor(
if (item is GradeDetailsItem) {
view?.apply {
showGradeDialog(item.grade)
if (item.grade.isNew) {
item.grade.isNew = false
if (!item.grade.isRead) {
item.grade.isRead = true
updateItem(item)
getHeaderOfItem(item)?.let { header ->
if (header is GradeDetailsHeader) {
@ -94,7 +95,7 @@ class GradeDetailsPresenter @Inject constructor(
subject = it.key,
average = formatAverage(average),
number = view?.getGradeNumberString(it.value.size).orEmpty(),
newGrades = it.value.filter { grade -> grade.isNew }.size
newGrades = it.value.filter { grade -> !grade.isRead }.size
).apply {
subItems = it.value.map { item ->
GradeDetailsItem(
@ -120,5 +121,6 @@ class GradeDetailsPresenter @Inject constructor(
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({}) { error -> errorHandler.proceed(error) })
Timber.d("Grade ${grade.id} updated")
}
}

View File

@ -20,4 +20,3 @@ class LoginErrorHandler(resources: Resources) : ErrorHandler(resources) {
doOnBadCredentials = {}
}
}

View File

@ -3,4 +3,4 @@ package io.github.wulkanowy.ui.modules.login
interface LoginSwitchListener {
fun switchFragment(position: Int)
}
}

View File

@ -10,6 +10,7 @@ import com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem
import com.ncapdevi.fragnav.FragNavController
import com.ncapdevi.fragnav.FragNavController.Companion.HIDE
import io.github.wulkanowy.R
import io.github.wulkanowy.services.notification.GradeNotification
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
@ -31,6 +32,7 @@ class MainActivity : BaseActivity(), MainView {
lateinit var navController: FragNavController
companion object {
const val EXTRA_CARD_ID_KEY = "cardId"
fun getStartIntent(context: Context) = Intent(context, MainActivity::class.java)
}
@ -51,7 +53,7 @@ class MainActivity : BaseActivity(), MainView {
setSupportActionBar(mainToolbar)
messageContainer = mainFragmentContainer
presenter.onAttachView(this)
presenter.onAttachView(this, intent.getIntExtra(EXTRA_CARD_ID_KEY, -1))
navController.initialize(startMenuIndex, savedInstanceState)
}
@ -66,13 +68,15 @@ class MainActivity : BaseActivity(), MainView {
override fun initView() {
mainBottomNav.run {
addItems(mutableListOf(
addItems(
mutableListOf(
AHBottomNavigationItem(R.string.grade_title, R.drawable.ic_menu_main_grade_26dp, 0),
AHBottomNavigationItem(R.string.attendance_title, R.drawable.ic_menu_main_attendance_24dp, 0),
AHBottomNavigationItem(R.string.exam_title, R.drawable.ic_menu_main_exam_24dp, 0),
AHBottomNavigationItem(R.string.timetable_title, R.drawable.ic_menu_main_timetable_24dp, 0),
AHBottomNavigationItem(R.string.more_title, R.drawable.ic_menu_main_more_24dp, 0)
))
)
)
accentColor = ContextCompat.getColor(context, R.color.colorPrimary)
inactiveColor = getThemeAttrColor(android.R.attr.textColorSecondary)
defaultBackgroundColor = getThemeAttrColor(R.attr.bottomNavBackground)
@ -89,11 +93,11 @@ class MainActivity : BaseActivity(), MainView {
setOnViewChangeListener { presenter.onViewStart() }
fragmentHideStrategy = HIDE
rootFragments = listOf(
GradeFragment.newInstance(),
AttendanceFragment.newInstance(),
ExamFragment.newInstance(),
TimetableFragment.newInstance(),
MoreFragment.newInstance()
GradeFragment.newInstance(),
AttendanceFragment.newInstance(),
ExamFragment.newInstance(),
TimetableFragment.newInstance(),
MoreFragment.newInstance()
)
}
}
@ -126,6 +130,10 @@ class MainActivity : BaseActivity(), MainView {
presenter.onBackPressed { super.onBackPressed() }
}
override fun cancelNotifications() {
GradeNotification(applicationContext).cancelAll()
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
navController.onSaveInstanceState(outState)

View File

@ -14,6 +14,7 @@ import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeModule
import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.settings.SettingsFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
@Module
@ -53,4 +54,7 @@ abstract class MainModule {
@PerFragment
@ContributesAndroidInjector(modules = [AboutModule::class])
abstract fun bindAboutFragment(): AboutFragment
@ContributesAndroidInjector
abstract fun bindSettingsFragment(): SettingsFragment
}

View File

@ -2,20 +2,26 @@ package io.github.wulkanowy.ui.modules.main
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.job.ServiceHelper
import io.github.wulkanowy.ui.base.BasePresenter
import javax.inject.Inject
class MainPresenter @Inject constructor(
errorHandler: ErrorHandler,
private val prefRepository: PreferencesRepository)
: BasePresenter<MainView>(errorHandler) {
errorHandler: ErrorHandler,
private val prefRepository: PreferencesRepository,
private val serviceHelper: ServiceHelper
) : BasePresenter<MainView>(errorHandler) {
override fun onAttachView(view: MainView) {
fun onAttachView(view: MainView, init: Int) {
super.onAttachView(view)
view.run {
startMenuIndex = prefRepository.startMenuIndex
cancelNotifications()
startMenuIndex = if (init != -1) init else prefRepository.startMenuIndex
initView()
}
serviceHelper.startFullSyncService()
}
fun onViewStart() {
@ -33,7 +39,6 @@ class MainPresenter @Inject constructor(
return true
}
fun onBackPressed(default: () -> Unit) {
view?.run {
if (isRootView) default()

View File

@ -24,6 +24,8 @@ interface MainView : BaseView {
fun popView()
fun cancelNotifications()
interface MainChildView {
fun onFragmentReselected()

View File

@ -1,14 +1,21 @@
package io.github.wulkanowy.ui.modules.settings
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate
import com.takisoft.preferencex.PreferenceFragmentCompat
import dagger.android.support.AndroidSupportInjection
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener,
MainView.TitledView {
MainView.TitledView, SettingsView {
@Inject
lateinit var presenter: SettingsPresenter
companion object {
fun newInstance() = SettingsFragment()
@ -17,19 +24,40 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
override val titleStringId: Int
get() = R.string.settings_title
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this)
}
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.scheme_preferences)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String) {
when(key) {
"theme" -> {
AppCompatDelegate.setDefaultNightMode(sharedPreferences?.getString("theme", "1")?.toInt() ?: 1)
activity?.recreate()
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
presenter.onSharedPreferenceChanged(key)
}
override fun setTheme(theme: Int) {
AppCompatDelegate.setDefaultNightMode(theme)
activity?.recreate()
}
override fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean) {
findPreference(serviceEnablesKey).run {
summary = if (isHolidays) getString(R.string.pref_services_suspended) else ""
isEnabled = !isHolidays
}
}
override fun showMessage(text: String) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)

View File

@ -0,0 +1,40 @@
package io.github.wulkanowy.ui.modules.settings
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.job.ServiceHelper
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.isHolidays
import org.threeten.bp.LocalDate.now
import javax.inject.Inject
class SettingsPresenter @Inject constructor(
errorHandler: ErrorHandler,
private val preferencesRepository: PreferencesRepository,
private val serviceHelper: ServiceHelper
) : BasePresenter<SettingsView>(errorHandler) {
override fun onAttachView(view: SettingsView) {
super.onAttachView(view)
view.run {
setServicesSuspended(preferencesRepository.serviceEnablesKey, now().isHolidays)
}
}
fun onSharedPreferenceChanged(key: String) {
when (key) {
preferencesRepository.serviceEnablesKey -> {
if (preferencesRepository.serviceEnabled) serviceHelper.startFullSyncService()
else serviceHelper.stopFullSyncService()
}
preferencesRepository.servicesIntervalKey,
preferencesRepository.servicesOnlyWifiKey -> {
serviceHelper.reloadFullSyncService()
}
preferencesRepository.currentThemeKey -> {
view?.setTheme(preferencesRepository.currentTheme)
}
}
}
}

View File

@ -0,0 +1,10 @@
package io.github.wulkanowy.ui.modules.settings
import io.github.wulkanowy.ui.base.BaseView
interface SettingsView : BaseView {
fun setTheme(theme: Int)
fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean)
}

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.splash
import android.os.Bundle
import io.github.wulkanowy.services.notification.GradeNotification
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.main.MainActivity

View File

@ -140,4 +140,3 @@ private fun generateKeyPair(context: Context) {
}
Timber.i("A new KeyPair has been generated")
}

View File

@ -62,6 +62,16 @@
</plurals>
<!--Grade notify-->
<string name="notify_grade_channel">Nowe oceny</string>
<plurals name="notify_grade_new_items">
<item quantity="one">Dostałeś %1$d ocenę</item>
<item quantity="few">"Dostałeś %1$d oceny</item>
<item quantity="many">Dostałeś %1$d ocen</item>
<item quantity="other">Dostałeś %1$d ocen</item>
</plurals>
<!--Timetable-->
<string name="timetable_lesson">Lekcja</string>
<string name="timetable_room">Sala</string>
@ -128,24 +138,12 @@
<string name="pref_notify_header">Powiadomienia</string>
<string name="pref_notify_switch">Pokazuj powiadomienia</string>
<string name="pref_services_header">Usługi</string>
<string name="pref_services_header">Synchronizacja</string>
<string name="pref_services_switch">Automatyczna aktualizacja</string>
<string name="pref_services_suspended">Zawieszone na wakacjach</string>
<string name="pref_services_suspended">Zawieszona na wakacjach</string>
<string name="pref_services_interval">Interwał aktualizacji</string>
<string name="pref_services_wifi">Tylko WiFi</string>
<string name="pref_restart">Wymagany restart</string>
<!--Grade notify-->
<string name="notify_grade_chanel">Nowe oceny</string>
<plurals name="notify_grade_new_items">
<item quantity="one">Dostałeś %1$d ocenę</item>
<item quantity="few">"Dostałeś %1$d oceny</item>
<item quantity="many">Dostałeś %1$d ocen</item>
<item quantity="other">Dostałeś %1$d ocen</item>
</plurals>
<!--Colors-->
<string name="all_black">Czarny</string>

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="services_interval_entries">
<item>10 minut</item>
<item>15 minut</item>
<item>30 minut</item>
<item>1 godzinę</item>
<item>1 godzina</item>
<item>2 godziny</item>
<item>6 godzin</item>
<item>12 godzin</item>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="pref_key_start_menu">start_menu</string>
<string name="pref_key_attendance_present">attendance_present</string>
<string name="pref_key_theme">theme</string>
<string name="pref_key_services_enable">services_enable</string>
<string name="pref_key_services_interval">services_interval</string>
<string name="pref_key_services_wifi_only">services_disable_wifi_only</string>
<string name="pref_key_notifications_enable">notifications_enable</string>
</resources>

View File

@ -57,6 +57,13 @@
<item quantity="other">New grades</item>
</plurals>
<!--Grade notify-->
<string name="notify_grade_channel">New grades</string>
<plurals name="notify_grade_new_items">
<item quantity="one">You received %1$d grade</item>
<item quantity="other">You received %1$d grades</item>
</plurals>
<!--Timetable-->
<string name="timetable_lesson">Lesson</string>
@ -124,22 +131,12 @@
<string name="pref_notify_header">Notifications</string>
<string name="pref_notify_switch">Show notifications</string>
<string name="pref_services_header">Services</string>
<string name="pref_services_header">Synchronization</string>
<string name="pref_services_switch">Automatic update</string>
<string name="pref_services_suspended">Suspended on holiday</string>
<string name="pref_services_interval">Updates interval</string>
<string name="pref_services_wifi">Only WiFi</string>
<string name="pref_restart">Restart required</string>
<!--Grade notify-->
<string name="notify_grade_chanel">New grades</string>
<plurals name="notify_grade_new_items">
<item quantity="one">You received %1$d grade</item>
<item quantity="other">You received %1$d grades</item>
</plurals>
<!--Colors-->
<string name="all_black">Black</string>

View File

@ -27,7 +27,7 @@
</string-array>
<string-array name="services_interval_entries">
<item>10 minutes</item>
<item>15 minutes</item>
<item>30 minutes</item>
<item>1 hour</item>
<item>2 hours</item>
@ -36,7 +36,7 @@
<item>24 hours</item>
</string-array>
<string-array name="services_interval_value" translatable="false">
<item>10</item>
<item>15</item>
<item>30</item>
<item>60</item>
<item>120</item>

View File

@ -1,29 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:title="@string/pref_view_header"
app:iconSpaceReserved="false">
android:title="@string/pref_view_header"
app:iconSpaceReserved="false">
<ListPreference
android:defaultValue="0"
android:entries="@array/startup_tab_entries"
android:entryValues="@array/startup_tab_value"
android:key="start_menu"
android:summary="%s"
android:title="@string/pref_view_list"
app:iconSpaceReserved="false"/>
android:defaultValue="0"
android:entries="@array/startup_tab_entries"
android:entryValues="@array/startup_tab_value"
android:key="@string/pref_key_start_menu"
android:summary="%s"
android:title="@string/pref_view_list"
app:iconSpaceReserved="false" />
<ListPreference
android:defaultValue="1"
android:entries="@array/theme_entries"
android:entryValues="@array/theme_values"
android:key="theme"
android:summary="%s"
android:title="@string/pref_view_theme_dark"
app:iconSpaceReserved="false" />
<SwitchPreference
android:defaultValue="true"
android:key="attendance_present"
android:title="@string/pref_view_present"
app:iconSpaceReserved="false"/>
android:defaultValue="true"
android:key="@string/pref_key_attendance_present"
android:title="@string/pref_view_present"
app:iconSpaceReserved="false" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/pref_services_header"
app:iconSpaceReserved="false">
<SwitchPreference
android:defaultValue="true"
android:key="@string/pref_key_services_enable"
android:title="@string/pref_services_switch"
app:iconSpaceReserved="false" />
<ListPreference
android:defaultValue="1"
android:entries="@array/theme_entries"
android:entryValues="@array/theme_values"
android:key="theme"
android:summary="%s"
android:title="@string/pref_view_theme_dark"
app:iconSpaceReserved="false"/>
android:defaultValue="60"
android:dependency="services_enable"
android:entries="@array/services_interval_entries"
android:entryValues="@array/services_interval_value"
android:key="@string/pref_key_services_interval"
android:summary="%s"
android:title="@string/pref_services_interval"
app:iconSpaceReserved="false" />
<SwitchPreference
android:defaultValue="false"
android:dependency="services_enable"
android:key="@string/pref_key_services_wifi_only"
android:title="@string/pref_services_wifi"
app:iconSpaceReserved="false" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/pref_notify_header"
app:iconSpaceReserved="false">
<SwitchPreference
android:defaultValue="true"
android:dependency="services_enable"
android:key="@string/pref_key_notifications_enable"
android:title="@string/pref_notify_switch"
app:iconSpaceReserved="false" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.ui.modules.main
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.services.job.ServiceHelper
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
@ -17,6 +18,9 @@ class MainPresenterTest {
@Mock
lateinit var prefRepository: PreferencesRepository
@Mock
lateinit var serviceHelper: ServiceHelper
@Mock
lateinit var mainView: MainView
@ -27,8 +31,8 @@ class MainPresenterTest {
MockitoAnnotations.initMocks(this)
clearInvocations(mainView)
presenter = MainPresenter(errorHandler, prefRepository)
presenter.onAttachView(mainView)
presenter = MainPresenter(errorHandler, prefRepository, serviceHelper)
presenter.onAttachView(mainView, -1)
}
@Test

View File

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.2.71'
ext.kotlin_version = '1.3.0'
repositories {
mavenCentral()
google()