Merge branch 'release/0.17.0'

This commit is contained in:
Mikołaj Pich 2020-04-05 18:42:36 +02:00
commit 232d8d38bd
116 changed files with 6552 additions and 555 deletions
.travis.yml
app
build.gradleproguard-rules.pro
schemas/io.github.wulkanowy.data.db.AppDatabase
src
debug/res/values
main
AndroidManifest.xml
java/io/github/wulkanowy
play/release-notes/pl-PL
res

@ -14,7 +14,7 @@ cache:
branches:
only:
- develop
- 0.16.0
- 0.17.0
android:
licenses:

@ -4,6 +4,7 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'io.fabric'
apply plugin: 'com.github.triplet.play'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply from: 'jacoco.gradle'
apply from: 'sonarqube.gradle'
apply from: 'hooks.gradle'
@ -17,8 +18,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 17
targetSdkVersion 29
versionCode 53
versionName "0.16.0"
versionCode 54
versionName "0.17.0"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@ -96,6 +97,10 @@ android {
exclude 'META-INF/library_release.kotlin_module'
exclude 'META-INF/library-core_release.kotlin_module'
}
aboutLibraries {
configPath = "app/src/main/res/raw"
}
}
androidExtensions {
@ -110,10 +115,11 @@ play {
}
ext {
work_manager = "2.3.2"
room = "2.2.4"
dagger = "2.26"
chucker = "2.0.4"
work_manager = "2.3.4"
room = "2.2.5"
dagger = "2.27"
// don't update https://github.com/ChuckerTeam/chucker/issues/242
chucker = "3.2.0"
mockk = "1.9.2"
}
@ -122,21 +128,21 @@ configurations.all {
}
dependencies {
implementation "io.github.wulkanowy:sdk:0.16.0"
implementation "io.github.wulkanowy:sdk:0.17.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "androidx.core:core-ktx:1.2.0"
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.appcompat:appcompat:1.2.0-beta01"
implementation "androidx.appcompat:appcompat-resources:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.2"
implementation "androidx.fragment:fragment-ktx:1.2.4"
implementation "androidx.annotation:annotation:1.1.0"
implementation "androidx.multidex:multidex:2.0.1"
implementation "androidx.preference:preference-ktx:1.1.0"
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.viewpager:viewpager:1.0.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-beta01"
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.1.0"
@ -148,6 +154,8 @@ dependencies {
implementation "androidx.work:work-rxjava2:$work_manager"
implementation "androidx.work:work-gcm:$work_manager"
implementation 'com.github.PaulinaSadowska:RxWorkManagerObservers:1.0.0'
implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-rxjava2:$room"
implementation "androidx.room:room-ktx:$room"
@ -163,34 +171,37 @@ dependencies {
implementation "eu.davidea:flexible-adapter-ui:1.0.0"
implementation "com.aurelhubert:ahbottomnavigation:2.3.4"
implementation "com.ncapdevi:frag-nav:3.3.0"
implementation "com.github.YarikSOffice:lingver:1.1.0"
implementation "com.github.YarikSOffice:lingver:1.2.1"
implementation "com.github.pwittchen:reactivenetwork-rx2:3.0.6"
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxjava:2.2.18"
implementation "io.reactivex.rxjava2:rxjava:2.2.19"
implementation "com.google.code.gson:gson:2.8.6"
implementation "com.jakewharton.threetenabp:threetenabp:1.2.2"
implementation "com.jakewharton.threetenabp:threetenabp:1.2.3"
implementation "com.jakewharton.timber:timber:4.7.1"
implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation "fr.bipi.treessence:treessence:0.3.2"
implementation "com.mikepenz:aboutlibraries-core:7.1.0"
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation 'com.wdullaer:materialdatetimepicker:4.2.3'
implementation "io.coil-kt:coil:0.9.5"
implementation("io.coil-kt:coil:0.9.5")
playImplementation "com.google.firebase:firebase-core:17.2.3"
playImplementation 'com.google.firebase:firebase-analytics:17.3.0'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.0.4'
playImplementation "com.google.firebase:firebase-inappmessaging-ktx:19.0.4"
playImplementation "com.google.firebase:firebase-messaging:20.1.0"
playImplementation "com.crashlytics.sdk.android:crashlytics:2.10.1"
playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
releaseImplementation "fr.o80.chucker:library-no-op:$chucker"
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
debugImplementation "fr.o80.chucker:library:$chucker"
debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker"
debugImplementation "com.amitshekhar.android:debug-db:1.0.6"
testImplementation "junit:junit:4.13"
testImplementation "io.mockk:mockk:$mockk"
testImplementation "org.threeten:threetenbp:1.4.1"
testImplementation "org.mockito:mockito-inline:3.3.1"
testImplementation "org.threeten:threetenbp:1.4.3"
testImplementation "org.mockito:mockito-inline:3.3.3"
androidTestImplementation "androidx.test:core:1.2.0"
androidTestImplementation "androidx.test:runner:1.2.0"
@ -198,7 +209,7 @@ dependencies {
androidTestImplementation "io.mockk:mockk-android:$mockk"
androidTestImplementation "androidx.room:room-testing:$room"
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
androidTestImplementation "org.mockito:mockito-android:3.3.1"
androidTestImplementation "org.mockito:mockito-android:3.3.3"
}
apply plugin: 'com.google.gms.google-services'

@ -43,3 +43,10 @@
#Config for Material Components
-keep class com.google.android.material.tabs.** { *; }
#Config for About Libraries
-keep class .R
-keep class **.R$* {
<fields>;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<bool name="pref_default_notification_debug">true</bool>
</resources>

@ -22,7 +22,8 @@
<activity
android:name=".ui.modules.splash.SplashActivity"
android:screenOrientation="portrait"
android:theme="@style/WulkanowyTheme.SplashScreen">
android:theme="@style/WulkanowyTheme.SplashScreen"
tools:ignore="LockedOrientationActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -112,5 +113,13 @@
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="${crashlytics_enabled}" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_push" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="push_channel" />
</application>
</manifest>

@ -7,9 +7,9 @@ import android.content.res.Resources
import androidx.preference.PreferenceManager
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.strategy.WalledGardenInternetObservingStrategy
import com.readystatesoftware.chuck.api.ChuckCollector
import com.readystatesoftware.chuck.api.ChuckInterceptor
import com.readystatesoftware.chuck.api.RetentionManager
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.chuckerteam.chucker.api.RetentionManager
import dagger.Module
import dagger.Provides
import io.github.wulkanowy.data.db.AppDatabase
@ -32,23 +32,25 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideSdk(chuckCollector: ChuckCollector, context: Context): Sdk {
fun provideSdk(chuckerCollector: ChuckerCollector, context: Context): Sdk {
return Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL
setSimpleHttpLogger { Timber.d(it) }
// for debug only
addInterceptor(ChuckInterceptor(context, chuckCollector).maxContentLength(250000L), true)
addInterceptor(ChuckerInterceptor(context, chuckerCollector), true)
}
}
@Singleton
@Provides
fun provideChuckCollector(context: Context, prefRepository: PreferencesRepository): ChuckCollector {
return ChuckCollector(context)
.showNotification(prefRepository.isDebugNotificationEnable)
.retentionManager(RetentionManager(context, ChuckCollector.Period.ONE_HOUR))
fun provideChuckerCollector(context: Context, prefRepository: PreferencesRepository): ChuckerCollector {
return ChuckerCollector(
context = context,
showNotification = prefRepository.isDebugNotificationEnable,
retentionPeriod = RetentionManager.Period.ONE_HOUR
)
}
@Singleton
@ -95,6 +97,10 @@ internal class RepositoryModule {
@Provides
fun provideMessagesDao(database: AppDatabase) = database.messagesDao
@Singleton
@Provides
fun provideMessageAttachmentsDao(database: AppDatabase) = database.messageAttachmentDao
@Singleton
@Provides
fun provideExamDao(database: AppDatabase) = database.examsDao

@ -17,6 +17,7 @@ import io.github.wulkanowy.data.db.dao.GradeStatisticsDao
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
import io.github.wulkanowy.data.db.dao.HomeworkDao
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.MobileDeviceDao
import io.github.wulkanowy.data.db.dao.NoteDao
@ -39,6 +40,7 @@ import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Recipient
@ -63,6 +65,9 @@ import io.github.wulkanowy.data.db.migrations.Migration2
import io.github.wulkanowy.data.db.migrations.Migration20
import io.github.wulkanowy.data.db.migrations.Migration21
import io.github.wulkanowy.data.db.migrations.Migration22
import io.github.wulkanowy.data.db.migrations.Migration23
import io.github.wulkanowy.data.db.migrations.Migration24
import io.github.wulkanowy.data.db.migrations.Migration25
import io.github.wulkanowy.data.db.migrations.Migration3
import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5
@ -86,6 +91,7 @@ import javax.inject.Singleton
GradeStatistics::class,
GradePointsStatistics::class,
Message::class,
MessageAttachment::class,
Note::class,
Homework::class,
Subject::class,
@ -104,7 +110,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 22
const val VERSION_SCHEMA = 25
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> {
return arrayOf(
@ -128,7 +134,10 @@ abstract class AppDatabase : RoomDatabase() {
Migration19(sharedPrefProvider),
Migration20(),
Migration21(),
Migration22()
Migration22(),
Migration23(),
Migration24(),
Migration25()
)
}
@ -164,6 +173,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract val messagesDao: MessagesDao
abstract val messageAttachmentDao: MessageAttachmentDao
abstract val noteDao: NoteDao
abstract val homeworkDao: HomeworkDao

@ -48,4 +48,14 @@ class Converters {
fun gsonToIntList(value: String): List<Int> {
return Gson().fromJson(value, object : TypeToken<List<Int>>() {}.type)
}
@TypeConverter
fun stringPairListToGson(list: List<Pair<String, String>>): String {
return Gson().toJson(list)
}
@TypeConverter
fun gsonToStringPairList(value: String): List<Pair<String, String>> {
return Gson().fromJson(value, object : TypeToken<List<Pair<String, String>>>() {}.type)
}
}

@ -0,0 +1,13 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import io.github.wulkanowy.data.db.entities.MessageAttachment
@Dao
interface MessageAttachmentDao : BaseDao<MessageAttachment> {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAttachments(items: List<MessageAttachment>): List<Long>
}

@ -2,19 +2,22 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.reactivex.Maybe
import io.reactivex.Single
@Dao
interface MessagesDao : BaseDao<Message> {
@Transaction
@Query("SELECT * FROM Messages WHERE student_id = :studentId AND message_id = :messageId")
fun loadMessageWithAttachment(studentId: Int, messageId: Int): Single<MessageWithAttachment>
@Query("SELECT * FROM Messages WHERE student_id = :studentId AND folder_id = :folder AND removed = 0 ORDER BY date DESC")
fun loadAll(studentId: Int, folder: Int): Maybe<List<Message>>
@Query("SELECT * FROM Messages WHERE id = :id")
fun load(id: Long): Single<Message>
@Query("SELECT * FROM Messages WHERE student_id = :studentId AND removed = 1 ORDER BY date DESC")
fun loadDeleted(studentId: Int): Maybe<List<Message>>
}

@ -27,10 +27,14 @@ data class Homework(
val teacher: String,
@ColumnInfo(name = "teacher_symbol")
val teacherSymbol: String
val teacherSymbol: String,
val attachments: List<Pair<String, String>>
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
@ColumnInfo(name = "is_done")
var isDone: Boolean = false
}

@ -44,7 +44,10 @@ data class Message(
@ColumnInfo(name = "read_by")
val readBy: Int,
val removed: Boolean
val removed: Boolean,
@ColumnInfo(name = "has_attachments")
val hasAttachments: Boolean
) : Serializable {
@PrimaryKey(autoGenerate = true)

@ -0,0 +1,26 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Entity(tableName = "MessageAttachments")
data class MessageAttachment(
@PrimaryKey
@ColumnInfo(name = "real_id")
val realId: Int,
@ColumnInfo(name = "message_id")
val messageId: Int,
@ColumnInfo(name = "one_drive_id")
val oneDriveId: String,
@ColumnInfo(name = "url")
val url: String,
@ColumnInfo(name = "filename")
val filename: String
) : Serializable

@ -0,0 +1,12 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.Embedded
import androidx.room.Relation
data class MessageWithAttachment(
@Embedded
val message: Message,
@Relation(parentColumn = "message_id", entityColumn = "message_id")
val attachments: List<MessageAttachment>
)

@ -16,8 +16,19 @@ data class Note(
val teacher: String,
@ColumnInfo(name = "teacher_symbol")
val teacherSymbol: String,
val category: String,
@ColumnInfo(name = "category_type")
val categoryType: Int,
@ColumnInfo(name = "is_points_show")
val isPointsShow: Boolean,
val points: Int,
val content: String
) : Serializable {

@ -0,0 +1,14 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration23 : Migration(22, 23) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Notes ADD COLUMN teacher_symbol TEXT NOT NULL DEFAULT ''")
database.execSQL("ALTER TABLE Notes ADD COLUMN category_type INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE Notes ADD COLUMN is_points_show INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE Notes ADD COLUMN points INTEGER NOT NULL DEFAULT 0")
}
}

@ -0,0 +1,21 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration24 : Migration(23, 24) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Messages ADD COLUMN has_attachments INTEGER NOT NULL DEFAULT 0")
database.execSQL("""
CREATE TABLE IF NOT EXISTS MessageAttachments (
real_id INTEGER NOT NULL,
message_id INTEGER NOT NULL,
one_drive_id TEXT NOT NULL,
url TEXT NOT NULL,
filename TEXT NOT NULL,
PRIMARY KEY(real_id)
)
""")
}
}

@ -0,0 +1,12 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration25 : Migration(24, 25) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Homework ADD COLUMN is_done INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE Homework ADD COLUMN attachments TEXT NOT NULL DEFAULT \"[]\"")
}
}

@ -19,6 +19,10 @@ class HomeworkLocal @Inject constructor(private val homeworkDb: HomeworkDao) {
homeworkDb.deleteAll(homework)
}
fun updateHomework(homework: List<Homework>) {
homeworkDb.updateAll(homework)
}
fun getHomework(semester: Semester, startDate: LocalDate, endDate: LocalDate): Maybe<List<Homework>> {
return homeworkDb.loadAll(semester.semesterId, semester.studentId, startDate, endDate)
.filter { it.isNotEmpty() }

@ -23,7 +23,8 @@ class HomeworkRemote @Inject constructor(private val sdk: Sdk) {
subject = it.subject,
content = it.content,
teacher = it.teacher,
teacherSymbol = it.teacherSymbol
teacherSymbol = it.teacherSymbol,
attachments = it.attachments.map { attachment -> attachment.url to attachment.name }
)
}
}

@ -7,6 +7,7 @@ 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.Completable
import io.reactivex.Single
import org.threeten.bp.LocalDate
import java.net.UnknownHostException
@ -36,4 +37,12 @@ class HomeworkRepository @Inject constructor(
}.flatMap { local.getHomework(semester, monday, friday).toSingle(emptyList()) })
}
}
fun toggleDone(homework: Homework): Completable {
return Completable.fromCallable {
local.updateHomework(listOf(homework.apply {
isDone = !isDone
}))
}
}
}

@ -2,6 +2,7 @@ package io.github.wulkanowy.data.repositories.luckynumber
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.SdkHelper
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.reactivex.Completable
@ -15,33 +16,36 @@ import javax.inject.Singleton
class LuckyNumberRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: LuckyNumberLocal,
private val remote: LuckyNumberRemote
private val remote: LuckyNumberRemote,
private val sdkHelper: SdkHelper
) {
fun getLuckyNumber(student: Student, forceRefresh: Boolean = false, notify: Boolean = false): Maybe<LuckyNumber> {
return local.getLuckyNumber(student, LocalDate.now()).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMapMaybe {
if (it) remote.getLuckyNumber(student)
else Maybe.error(UnknownHostException())
}.flatMap { new ->
local.getLuckyNumber(student, LocalDate.now())
.doOnSuccess { old ->
if (new != old) {
local.deleteLuckyNumber(old)
return Maybe.just(sdkHelper.init(student)).flatMap {
local.getLuckyNumber(student, LocalDate.now()).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMapMaybe {
if (it) remote.getLuckyNumber(student)
else Maybe.error(UnknownHostException())
}.flatMap { new ->
local.getLuckyNumber(student, LocalDate.now())
.doOnSuccess { old ->
if (new != old) {
local.deleteLuckyNumber(old)
local.saveLuckyNumber(new.apply {
if (notify) isNotified = false
})
}
}
.doOnComplete {
local.saveLuckyNumber(new.apply {
if (notify) isNotified = false
})
}
}
.doOnComplete {
local.saveLuckyNumber(new.apply {
if (notify) isNotified = false
})
}
}.flatMap({ local.getLuckyNumber(student, LocalDate.now()) }, { Maybe.error(it) },
{ local.getLuckyNumber(student, LocalDate.now()) })
)
}.flatMap({ local.getLuckyNumber(student, LocalDate.now()) }, { Maybe.error(it) },
{ local.getLuckyNumber(student, LocalDate.now()) })
)
}
}
fun getNotNotifiedLuckyNumber(student: Student): Maybe<LuckyNumber> {

@ -1,7 +1,10 @@
package io.github.wulkanowy.data.repositories.message
import io.github.wulkanowy.data.db.dao.MessageAttachmentDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.message.MessageFolder.TRASHED
import io.reactivex.Maybe
@ -10,7 +13,10 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MessageLocal @Inject constructor(private val messagesDb: MessagesDao) {
class MessageLocal @Inject constructor(
private val messagesDb: MessagesDao,
private val messageAttachmentDao: MessageAttachmentDao
) {
fun saveMessages(messages: List<Message>) {
messagesDb.insertAll(messages)
@ -24,8 +30,12 @@ class MessageLocal @Inject constructor(private val messagesDb: MessagesDao) {
messagesDb.deleteAll(messages)
}
fun getMessage(id: Long): Single<Message> {
return messagesDb.load(id)
fun getMessageWithAttachment(student: Student, message: Message): Single<MessageWithAttachment> {
return messagesDb.loadMessageWithAttachment(student.id.toInt(), message.messageId)
}
fun saveMessageAttachments(attachments: List<MessageAttachment>) {
messageAttachmentDao.insertAttachments(attachments)
}
fun getMessages(student: Student, folder: MessageFolder): Maybe<List<Message>> {

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.repositories.message
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
@ -33,14 +34,25 @@ class MessageRemote @Inject constructor(private val sdk: Sdk) {
unread = it.unread ?: false,
unreadBy = it.unreadBy ?: 0,
readBy = it.readBy ?: 0,
removed = it.removed
removed = it.removed,
hasAttachments = it.hasAttachments
)
}
}
}
fun getMessagesContent(message: Message, markAsRead: Boolean = false): Single<String> {
return sdk.getMessageContent(message.messageId, message.folderId, markAsRead, message.realId)
fun getMessagesContentDetails(message: Message, markAsRead: Boolean = false): Single<Pair<String, List<MessageAttachment>>> {
return sdk.getMessageDetails(message.messageId, message.folderId, markAsRead, message.realId).map { details ->
details.content to details.attachments.map {
MessageAttachment(
realId = it.id,
messageId = it.messageId,
oneDriveId = it.oneDriveId,
url = it.url,
filename = it.filename
)
}
}
}
fun sendMessage(subject: String, content: String, recipients: List<Recipient>): Single<SentMessage> {

@ -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.SdkHelper
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
@ -11,7 +12,6 @@ import io.github.wulkanowy.data.repositories.message.MessageFolder.RECEIVED
import io.github.wulkanowy.sdk.pojo.SentMessage
import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Completable
import io.reactivex.Maybe
import io.reactivex.Single
import timber.log.Timber
import java.net.UnknownHostException
@ -48,30 +48,31 @@ class MessageRepository @Inject constructor(
}
}
fun getMessage(student: Student, messageDbId: Long, markAsRead: Boolean = false): Single<Message> {
fun getMessage(student: Student, message: Message, markAsRead: Boolean = false): Single<MessageWithAttachment> {
return Single.just(sdkHelper.init(student))
.flatMap { _ ->
local.getMessage(messageDbId)
local.getMessageWithAttachment(student, message)
.filter {
it.content.isNotEmpty().also { status ->
it.message.content.isNotEmpty().also { status ->
Timber.d("Message content in db empty: ${!status}")
}
} && !it.message.unread
}
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) local.getMessage(messageDbId)
if (it) local.getMessageWithAttachment(student, message)
else Single.error(UnknownHostException())
}
.flatMap { dbMessage ->
remote.getMessagesContent(dbMessage, markAsRead).doOnSuccess {
local.updateMessages(listOf(dbMessage.copy(unread = !markAsRead).apply {
id = dbMessage.id
content = content.ifBlank { it }
remote.getMessagesContentDetails(dbMessage.message, markAsRead).doOnSuccess { (downloadedMessage, attachments) ->
local.updateMessages(listOf(dbMessage.message.copy(unread = !markAsRead).apply {
id = dbMessage.message.id
content = content.ifBlank { downloadedMessage }
}))
Timber.d("Message $messageDbId with blank content: ${dbMessage.content.isBlank()}, marked as read")
local.saveMessageAttachments(attachments)
Timber.d("Message ${message.messageId} with blank content: ${dbMessage.message.content.isBlank()}, marked as read")
}
}.flatMap {
local.getMessage(messageDbId)
local.getMessageWithAttachment(student, message)
}
)
}
@ -83,10 +84,6 @@ class MessageRepository @Inject constructor(
.toSingle(emptyList())
}
fun updateMessage(message: Message): Completable {
return Completable.fromCallable { local.updateMessages(listOf(message)) }
}
fun updateMessages(messages: List<Message>): Completable {
return Completable.fromCallable { local.updateMessages(messages) }
}
@ -99,13 +96,12 @@ class MessageRepository @Inject constructor(
}
}
fun deleteMessage(message: Message): Maybe<Boolean> {
fun deleteMessage(message: Message): Single<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

@ -18,7 +18,11 @@ class NoteRemote @Inject constructor(private val sdk: Sdk) {
studentId = semester.studentId,
date = it.date,
teacher = it.teacher,
teacherSymbol = it.teacherSymbol,
category = it.category,
categoryType = it.categoryType.id,
isPointsShow = it.showPoints,
points = it.points,
content = it.content
)
}

@ -15,6 +15,7 @@ import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel
import io.github.wulkanowy.services.sync.channels.NewGradesChannel
import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
import io.github.wulkanowy.services.sync.channels.NewNotesChannel
import io.github.wulkanowy.services.sync.channels.PushChannel
import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork
import io.github.wulkanowy.services.sync.works.AttendanceWork
import io.github.wulkanowy.services.sync.works.CompletedLessonWork
@ -126,4 +127,8 @@ abstract class ServicesModule {
@Binds
@IntoSet
abstract fun provideNewNotesChannel(channel: NewNotesChannel): Channel
@Binds
@IntoSet
abstract fun providePushChannel(channel: PushChannel): Channel
}

@ -5,18 +5,24 @@ import android.os.Build.VERSION_CODES.O
import androidx.core.app.NotificationManagerCompat
import androidx.work.BackoffPolicy.EXPONENTIAL
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy.KEEP
import androidx.work.ExistingPeriodicWorkPolicy.REPLACE
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType.CONNECTED
import androidx.work.NetworkType.UNMETERED
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.paulinasadowska.rxworkmanagerobservers.extensions.getWorkInfoByIdObservable
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.SharedPrefProvider.Companion.APP_VERSION_CODE_KEY
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.Channel
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.isHolidays
import io.reactivex.Observable
import org.threeten.bp.LocalDate.now
import timber.log.Timber
import java.util.concurrent.TimeUnit.MINUTES
@ -42,13 +48,13 @@ class SyncManager @Inject constructor(
}
if (sharedPrefProvider.getLong(APP_VERSION_CODE_KEY, -1L) != appInfo.versionCode.toLong()) {
startSyncWorker(true)
startPeriodicSyncWorker(true)
sharedPrefProvider.putLong(APP_VERSION_CODE_KEY, appInfo.versionCode.toLong(), true)
}
Timber.i("SyncManager was initialized")
}
fun startSyncWorker(restart: Boolean = false) {
fun startPeriodicSyncWorker(restart: Boolean = false) {
if (preferencesRepository.isServiceEnabled && !now().isHolidays) {
workManager.enqueueUniquePeriodicWork(SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP,
PeriodicWorkRequestBuilder<SyncWorker>(preferencesRepository.servicesInterval, MINUTES)
@ -61,6 +67,19 @@ class SyncManager @Inject constructor(
}
}
fun startOneTimeSyncWorker(): Observable<WorkInfo> {
val work = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(
Data.Builder()
.putBoolean("one_time", true)
.build()
)
.build()
workManager.enqueueUniqueWork("${SyncWorker::class.java.simpleName}_one_time", ExistingWorkPolicy.REPLACE, work)
return workManager.getWorkInfoByIdObservable(work.id)
}
fun stopSyncWorker() {
workManager.cancelUniqueWork(SyncWorker::class.java.simpleName)
}

@ -1,10 +1,12 @@
package io.github.wulkanowy.services.sync
import android.content.Context
import android.os.Build.VERSION_CODES.LOLLIPOP
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.BigTextStyle
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
import androidx.core.app.NotificationManagerCompat
import androidx.work.Data
import androidx.work.ListenableWorker
import androidx.work.RxWorker
import androidx.work.WorkerParameters
@ -17,6 +19,7 @@ import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.sdk.exception.FeatureDisabledException
import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.works.Work
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.getCompatColor
import io.reactivex.Completable
import io.reactivex.Single
@ -30,7 +33,8 @@ class SyncWorker @AssistedInject constructor(
private val semesterRepository: SemesterRepository,
private val works: Set<@JvmSuppressWildcards Work>,
private val preferencesRepository: PreferencesRepository,
private val notificationManager: NotificationManagerCompat
private val notificationManager: NotificationManagerCompat,
private val appInfo: AppInfo
) : RxWorker(appContext, workerParameters) {
override fun createWork(): Single<Result> {
@ -52,8 +56,15 @@ class SyncWorker @AssistedInject constructor(
.toSingleDefault(Result.success())
.onErrorReturn {
Timber.e(it, "There was an error during synchronization")
if (it is FeatureDisabledException) Result.success()
else Result.retry()
when {
it is FeatureDisabledException -> Result.success()
inputData.getBoolean("one_time", false) -> {
Result.failure(Data.Builder()
.putString("error", it.toString())
.build())
}
else -> Result.retry()
}
}
.doOnSuccess {
if (preferencesRepository.isDebugNotificationEnable) notify(it)
@ -64,7 +75,7 @@ class SyncWorker @AssistedInject constructor(
private fun notify(result: Result) {
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(applicationContext, DebugChannel.CHANNEL_ID)
.setContentTitle("Debug notification")
.setSmallIcon(R.drawable.ic_more_settings)
.setSmallIcon(R.drawable.ic_stat_push)
.setAutoCancel(true)
.setColor(applicationContext.getCompatColor(R.color.colorPrimary))
.setStyle(BigTextStyle().bigText("${SyncWorker::class.java.simpleName} result: $result"))

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

@ -1,7 +1,7 @@
package io.github.wulkanowy.ui.base
import android.content.res.Resources
import com.readystatesoftware.chuck.api.ChuckCollector
import com.chuckerteam.chucker.api.ChuckerCollector
import io.github.wulkanowy.R
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.sdk.exception.BadCredentialsException
@ -15,7 +15,7 @@ import java.net.SocketTimeoutException
import java.net.UnknownHostException
import javax.inject.Inject
open class ErrorHandler @Inject constructor(protected val resources: Resources, private val chuckCollector: ChuckCollector) {
open class ErrorHandler @Inject constructor(protected val resources: Resources, private val chuckerCollector: ChuckerCollector) {
var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> }
@ -24,7 +24,7 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources,
var onNoCurrentStudent: () -> Unit = {}
fun dispatch(error: Throwable) {
chuckCollector.onError(error.javaClass.simpleName, error)
chuckerCollector.onError(error.javaClass.simpleName, error)
Timber.e(error, "An exception occurred while the Wulkanowy was running")
proceed(error)
}

@ -1,12 +1,6 @@
package io.github.wulkanowy.ui.modules.about
import android.content.Intent
import android.content.Intent.ACTION_SENDTO
import android.content.Intent.EXTRA_EMAIL
import android.content.Intent.EXTRA_SUBJECT
import android.content.Intent.EXTRA_TEXT
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -23,6 +17,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.getCompatDrawable
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_about.*
@ -124,26 +119,17 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
}
override fun openEmailClient() {
val intent = Intent(ACTION_SENDTO)
.apply {
data = Uri.parse("mailto:")
putExtra(EXTRA_EMAIL, arrayOf("wulkanowyinc@gmail.com"))
putExtra(EXTRA_SUBJECT, "Zgłoszenie błędu")
putExtra(EXTRA_TEXT, "Tu umieść treść zgłoszenia\n\n${"-".repeat(40)}\n " +
"""
Build: ${appInfo.versionCode}
SDK: ${appInfo.systemVersion}
Device: ${appInfo.systemManufacturer} ${appInfo.systemModel}
""".trimIndent())
requireContext().openEmailClient(
chooserTitle = getString(R.string.about_feedback),
email = "wulkanowyinc@gmail.com",
subject = "Zgłoszenie błędu",
body = requireContext().getString(R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName
),
onActivityNotFound = {
requireContext().openInternetBrowser("https://github.com/wulkanowy/wulkanowy/issues", ::showMessage)
}
context?.let {
if (intent.resolveActivity(it.packageManager) != null) {
startActivity(Intent.createChooser(intent, getString(R.string.about_feedback)))
} else {
it.openInternetBrowser("https://github.com/wulkanowy/wulkanowy/issues", ::showMessage)
}
}
)
}
override fun openFaqPage() {

@ -19,7 +19,7 @@ class LicenseItem(val library: Library) : AbstractFlexibleItem<LicenseItem.ViewH
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
with(holder) {
licenseItemName.text = library.libraryName
licenseItemSummary.text = library.license?.licenseName
licenseItemSummary.text = library.license?.licenseName?.takeIf { it.isNotBlank() } ?: library.libraryVersion
}
}

@ -27,10 +27,6 @@ class LicensePresenter @Inject constructor(
private fun loadData() {
disposable.add(Single.fromCallable { view?.appLibraries }
.map {
val exclude = listOf("Android-Iconics", "CircleImageView", "FastAdapter", "Jsoup", "okio", "Retrofit")
it.filter { library -> !exclude.contains(library.libraryName) }
}
.map { it.map { library -> LicenseItem(library) } }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)

@ -1,49 +0,0 @@
package io.github.wulkanowy.ui.modules.homework
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.synthetic.main.dialog_homework.*
class HomeworkDialog : DialogFragment() {
private lateinit var homework: Homework
companion object {
private const val ARGUMENT_KEY = "Item"
fun newInstance(homework: Homework): HomeworkDialog {
return HomeworkDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, homework) }
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
homework = getSerializable(HomeworkDialog.ARGUMENT_KEY) as Homework
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_homework, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
homeworkDialogDate.text = homework.date.toFormattedString()
homeworkDialogEntryDate.text = homework.entryDate.toFormattedString()
homeworkDialogSubject.text = homework.subject
homeworkDialogTeacher.text = homework.teacher
homeworkDialogContent.text = homework.content
homeworkDialogClose.setOnClickListener { dismiss() }
}
}

@ -13,6 +13,7 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.homework.details.HomeworkDetailsDialog
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.dpToPx
@ -73,6 +74,10 @@ class HomeworkFragment : BaseFragment(), HomeworkView, MainView.TitledView {
homeworkAdapter.updateDataSet(data, true)
}
fun onReloadList() {
presenter.reloadData()
}
override fun clearData() {
homeworkAdapter.clear()
}
@ -118,7 +123,7 @@ class HomeworkFragment : BaseFragment(), HomeworkView, MainView.TitledView {
}
override fun showTimetableDialog(homework: Homework) {
(activity as? MainActivity)?.showDialogFragment(HomeworkDialog.newInstance(homework))
(activity as? MainActivity)?.showDialogFragment(HomeworkDetailsDialog.newInstance(homework))
}
override fun onSaveInstanceState(outState: Bundle) {

@ -24,6 +24,8 @@ class HomeworkItem(header: HomeworkHeader, val homework: Homework) :
homeworkItemSubject.text = homework.subject
homeworkItemTeacher.text = homework.teacher
homeworkItemContent.text = homework.content
homeworkItemCheckImage.visibility = if (homework.isDone) View.VISIBLE else View.GONE
homeworkItemAttachmentImage.visibility = if (!homework.isDone && homework.attachments.isNotEmpty()) View.VISIBLE else View.GONE
}
}
@ -43,7 +45,8 @@ class HomeworkItem(header: HomeworkHeader, val homework: Homework) :
return result
}
class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter), LayoutContainer {
override val containerView: View
get() = contentView
}

@ -95,6 +95,10 @@ class HomeworkPresenter @Inject constructor(
})
}
fun reloadData() {
loadData(currentDate, false)
}
private fun loadData(date: LocalDate, forceRefresh: Boolean = false) {
Timber.i("Loading homework data started")
currentDate = date

@ -0,0 +1,79 @@
package io.github.wulkanowy.ui.modules.homework.details
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.text.HtmlCompat
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.synthetic.main.dialog_homework.*
import javax.inject.Inject
class HomeworkDetailsDialog : BaseDialogFragment(), HomeworkDetailsView {
@Inject
lateinit var presenter: HomeworkDetailsPresenter
private lateinit var homework: Homework
companion object {
private const val ARGUMENT_KEY = "Item"
fun newInstance(homework: Homework): HomeworkDetailsDialog {
return HomeworkDetailsDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, homework) }
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
homework = getSerializable(ARGUMENT_KEY) as Homework
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_homework, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this)
}
@SuppressLint("SetTextI18n")
override fun initView() {
homeworkDialogDate.text = homework.date.toFormattedString()
homeworkDialogEntryDate.text = homework.entryDate.toFormattedString()
homeworkDialogSubject.text = homework.subject
homeworkDialogTeacher.text = homework.teacher
homeworkDialogContent.movementMethod = LinkMovementMethod.getInstance()
homeworkDialogContent.text = homework.content
homeworkDialogAttachments.movementMethod = LinkMovementMethod.getInstance()
homeworkDialogAttachments.text = HtmlCompat.fromHtml(homework.attachments.joinToString("<br>") {
"<a href='${it.first}'>${it.second}</a>"
}, HtmlCompat.FROM_HTML_MODE_COMPACT).ifBlank { getString(R.string.all_no_data) }
homeworkDialogRead.text = view?.context?.getString(if (homework.isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done)
homeworkDialogRead.setOnClickListener { presenter.toggleDone(homework) }
homeworkDialogClose.setOnClickListener { dismiss() }
}
override fun updateMarkAsDoneLabel(isDone: Boolean) {
(parentFragment as? HomeworkFragment)?.onReloadList()
homeworkDialogRead.text = view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done)
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

@ -0,0 +1,44 @@
package io.github.wulkanowy.ui.modules.homework.details
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.repositories.homework.HomeworkRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import timber.log.Timber
import javax.inject.Inject
class HomeworkDetailsPresenter @Inject constructor(
schedulers: SchedulersProvider,
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val homeworkRepository: HomeworkRepository,
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<HomeworkDetailsView>(errorHandler, studentRepository, schedulers) {
override fun onAttachView(view: HomeworkDetailsView) {
super.onAttachView(view)
view.initView()
Timber.i("Homework details view was initialized")
}
fun toggleDone(homework: Homework) {
Timber.i("Homework details update start")
disposable.add(homeworkRepository.toggleDone(homework)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
Timber.i("Homework details update: Success")
view?.run {
updateMarkAsDoneLabel(homework.isDone)
}
analytics.logEvent("homework_mark_as_done")
}) {
Timber.i("Homework details update result: An exception occurred")
errorHandler.dispatch(it)
}
)
}
}

@ -0,0 +1,10 @@
package io.github.wulkanowy.ui.modules.homework.details
import io.github.wulkanowy.ui.base.BaseView
interface HomeworkDetailsView : BaseView {
fun initView()
fun updateMarkAsDoneLabel(isDone: Boolean)
}

@ -2,7 +2,7 @@ package io.github.wulkanowy.ui.modules.login
import android.content.res.Resources
import android.database.sqlite.SQLiteConstraintException
import com.readystatesoftware.chuck.api.ChuckCollector
import com.chuckerteam.chucker.api.ChuckerCollector
import io.github.wulkanowy.R
import io.github.wulkanowy.sdk.exception.BadCredentialsException
import io.github.wulkanowy.sdk.mobile.exception.InvalidPinException
@ -14,8 +14,8 @@ import javax.inject.Inject
class LoginErrorHandler @Inject constructor(
resources: Resources,
chuckCollector: ChuckCollector
) : ErrorHandler(resources, chuckCollector) {
chuckerCollector: ChuckerCollector
) : ErrorHandler(resources, chuckerCollector) {
var onBadCredentials: () -> Unit = {}

@ -146,6 +146,20 @@ class LoginAdvancedFragment : BaseFragment(), LoginAdvancedView {
}
}
override fun setErrorLoginRequired() {
with(loginFormUsernameLayout) {
requestFocus()
error = getString(R.string.login_invalid_login)
}
}
override fun setErrorEmailRequired() {
with(loginFormUsernameLayout) {
requestFocus()
error = getString(R.string.login_invalid_email)
}
}
override fun setErrorPassRequired(focus: Boolean) {
with(loginFormPassLayout) {
if (focus) requestFocus()

@ -173,6 +173,8 @@ class LoginAdvancedPresenter @Inject constructor(
val login = view?.formUsernameValue.orEmpty()
val password = view?.formPassValue.orEmpty()
val host = view?.formHostValue.orEmpty()
val pin = view?.formPinValue.orEmpty()
val symbol = view?.formSymbolValue.orEmpty()
val token = view?.formTokenValue.orEmpty()
@ -196,26 +198,20 @@ class LoginAdvancedPresenter @Inject constructor(
isCorrect = false
}
}
Sdk.Mode.SCRAPPER -> {
Sdk.Mode.HYBRID, Sdk.Mode.SCRAPPER -> {
if (login.isEmpty()) {
view?.setErrorUsernameRequired()
isCorrect = false
}
} else {
if ("@" in login && "standard" !in host) {
view?.setErrorLoginRequired()
isCorrect = false
}
if (password.isEmpty()) {
view?.setErrorPassRequired(focus = isCorrect)
isCorrect = false
}
if (password.length < 6 && password.isNotEmpty()) {
view?.setErrorPassInvalid(focus = isCorrect)
isCorrect = false
}
}
Sdk.Mode.HYBRID -> {
if (login.isEmpty()) {
view?.setErrorUsernameRequired()
isCorrect = false
if ("@" !in login && "standard" in host) {
view?.setErrorEmailRequired()
isCorrect = false
}
}
if (password.isEmpty()) {

@ -41,6 +41,10 @@ interface LoginAdvancedView : BaseView {
fun setErrorUsernameRequired()
fun setErrorLoginRequired()
fun setErrorEmailRequired()
fun setErrorPassRequired(focus: Boolean)
fun setErrorPassInvalid(focus: Boolean)

@ -121,6 +121,20 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
}
}
override fun setErrorLoginRequired() {
with(loginFormUsernameLayout) {
requestFocus()
error = getString(R.string.login_invalid_login)
}
}
override fun setErrorEmailRequired() {
with(loginFormUsernameLayout) {
requestFocus()
error = getString(R.string.login_invalid_email)
}
}
override fun setErrorSymbolRequired(focus: Boolean) {
with(loginFormSymbolLayout) {
if (focus) requestFocus()
@ -219,12 +233,18 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
}
}
override fun openEmail() {
override fun openEmail(lastError: String) {
context?.openEmailClient(
requireContext().getString(R.string.login_email_intent_title),
"wulkanowyinc@gmail.com",
requireContext().getString(R.string.login_email_subject),
requireContext().getString(R.string.login_email_text, appInfo.systemModel, appInfo.systemVersion.toString(), appInfo.versionName)
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
appInfo.versionName,
"$formHostValue/$formSymbolValue",
lastError
)
)
}
}

@ -16,6 +16,8 @@ class LoginFormPresenter @Inject constructor(
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<LoginFormView>(loginErrorHandler, studentRepository, schedulers) {
private var lastError: Throwable? = null
override fun onAttachView(view: LoginFormView) {
super.onAttachView(view)
view.run {
@ -109,6 +111,7 @@ class LoginFormPresenter @Inject constructor(
Timber.i("Login result: An exception occurred")
analytics.logEvent("registration_form", "success" to false, "students" to -1, "scrapperBaseUrl" to host, "error" to it.message.ifNullOrBlank { "No message" })
loginErrorHandler.dispatch(it)
lastError = it
view?.showContact(true)
}))
}
@ -118,7 +121,7 @@ class LoginFormPresenter @Inject constructor(
}
fun onEmailClick() {
view?.openEmail()
view?.openEmail(lastError?.message.ifNullOrBlank { "none" })
}
fun onRecoverClick() {
@ -131,6 +134,16 @@ class LoginFormPresenter @Inject constructor(
if (login.isEmpty()) {
view?.setErrorUsernameRequired()
isCorrect = false
} else {
if ("@" in login && "standard" !in host) {
view?.setErrorLoginRequired()
isCorrect = false
}
if ("@" !in login && "standard" in host) {
view?.setErrorEmailRequired()
isCorrect = false
}
}
if (password.isEmpty()) {

@ -31,6 +31,10 @@ interface LoginFormView : BaseView {
fun setErrorUsernameRequired()
fun setErrorLoginRequired()
fun setErrorEmailRequired()
fun setErrorSymbolRequired(focus: Boolean)
fun setErrorPassRequired(focus: Boolean)
@ -63,7 +67,7 @@ interface LoginFormView : BaseView {
fun openFaqPage()
fun openEmail()
fun openEmail(lastError: String)
fun openAdvancedLogin()

@ -1,7 +1,7 @@
package io.github.wulkanowy.ui.modules.login.recover
import android.content.res.Resources
import com.readystatesoftware.chuck.api.ChuckCollector
import com.chuckerteam.chucker.api.ChuckerCollector
import io.github.wulkanowy.sdk.scrapper.exception.InvalidCaptchaException
import io.github.wulkanowy.sdk.scrapper.exception.InvalidEmailException
import io.github.wulkanowy.sdk.scrapper.exception.NoAccountFoundException
@ -10,8 +10,8 @@ import javax.inject.Inject
class RecoverErrorHandler @Inject constructor(
resources: Resources,
chuckCollector: ChuckCollector
) : ErrorHandler(resources, chuckCollector) {
chuckerCollector: ChuckerCollector
) : ErrorHandler(resources, chuckerCollector) {
var onInvalidUsername: (String) -> Unit = {}

@ -101,12 +101,17 @@ class LoginStudentSelectFragment : BaseFragment(), LoginStudentSelectView {
context?.openInternetBrowser("https://discord.gg/vccAQBr", ::showMessage)
}
override fun openEmail() {
override fun openEmail(lastError: String) {
context?.openEmailClient(
requireContext().getString(R.string.login_email_intent_title),
"wulkanowyinc@gmail.com",
requireContext().getString(R.string.login_email_subject),
requireContext().getString(R.string.login_email_text, appInfo.systemModel, appInfo.systemVersion.toString(), appInfo.versionName)
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(R.string.login_email_text, appInfo.systemModel,
appInfo.systemVersion.toString(),
appInfo.versionName,
"Select users to log in",
lastError
)
)
}
}

@ -8,7 +8,6 @@ import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.ifNullOrBlank
import io.reactivex.Single
import timber.log.Timber
import java.io.Serializable
import javax.inject.Inject
@ -20,6 +19,8 @@ class LoginStudentSelectPresenter @Inject constructor(
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository, schedulers) {
private var lastError: Throwable? = null
var students = emptyList<Student>()
private var selectedStudents = mutableListOf<Student>()
@ -83,6 +84,7 @@ class LoginStudentSelectPresenter @Inject constructor(
})
}, {
errorHandler.dispatch(it)
lastError = it
view?.updateData(students.map { student -> LoginStudentSelectItem(student, false) })
})
)
@ -109,6 +111,7 @@ class LoginStudentSelectPresenter @Inject constructor(
students.forEach { analytics.logEvent("registration_student_select", "success" to false, "scrapperBaseUrl" to it.scrapperBaseUrl, "symbol" to it.symbol, "error" to error.message.ifNullOrBlank { "No message" }) }
Timber.i("Registration result: An exception occurred ")
loginErrorHandler.dispatch(error)
lastError = error
view?.apply {
showProgress(false)
showContent(true)
@ -122,6 +125,6 @@ class LoginStudentSelectPresenter @Inject constructor(
}
fun onEmailClick() {
view?.openEmail()
view?.openEmail(lastError?.message.ifNullOrBlank { "empty" })
}
}

@ -20,5 +20,5 @@ interface LoginStudentSelectView : BaseView {
fun openDiscordInvite()
fun openEmail()
fun openEmail(lastError: String)
}

@ -130,12 +130,18 @@ class LoginSymbolFragment : BaseFragment(), LoginSymbolView {
context?.openInternetBrowser("https://wulkanowy.github.io/czesto-zadawane-pytania/co-to-jest-symbol", ::showMessage)
}
override fun openEmail() {
override fun openEmail(host: String, lastError: String) {
context?.openEmailClient(
requireContext().getString(R.string.login_email_intent_title),
"wulkanowyinc@gmail.com",
requireContext().getString(R.string.login_email_subject),
requireContext().getString(R.string.login_email_text, appInfo.systemModel, appInfo.systemVersion.toString(), appInfo.versionName)
chooserTitle = requireContext().getString(R.string.login_email_intent_title),
email = "wulkanowyinc@gmail.com",
subject = requireContext().getString(R.string.login_email_subject),
body = requireContext().getString(R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
appInfo.versionName,
"$host/${loginSymbolName.text}",
lastError
)
)
}
}

@ -18,6 +18,8 @@ class LoginSymbolPresenter @Inject constructor(
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<LoginSymbolView>(loginErrorHandler, studentRepository, schedulers) {
private var lastError: Throwable? = null
var loginData: Triple<String, String, String>? = null
@Suppress("UNCHECKED_CAST")
@ -37,13 +39,15 @@ class LoginSymbolPresenter @Inject constructor(
}
fun attemptLogin(symbol: String) {
if (loginData == null) throw IllegalArgumentException("Login data is null")
if (symbol.isBlank()) {
view?.setErrorSymbolRequire()
return
}
disposable.add(
Single.fromCallable { if (loginData == null) throw IllegalArgumentException("Login data is null") else loginData }
Single.fromCallable { loginData }
.flatMap { studentRepository.getStudentsScrapper(it.first, it.second, it.third, symbol) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
@ -77,6 +81,7 @@ class LoginSymbolPresenter @Inject constructor(
Timber.i("Login with symbol result: An exception occurred")
analytics.logEvent("registration_symbol", "success" to false, "students" to -1, "scrapperBaseUrl" to loginData?.third, "symbol" to symbol, "error" to it.message.ifNullOrBlank { "No message" })
loginErrorHandler.dispatch(it)
lastError = it
view?.showContact(true)
}))
}
@ -94,6 +99,6 @@ class LoginSymbolPresenter @Inject constructor(
}
fun onEmailClick() {
view?.openEmail()
view?.openEmail(loginData?.third.orEmpty(), lastError?.message.ifNullOrBlank { "empty" })
}
}

@ -31,5 +31,5 @@ interface LoginSymbolView : BaseView {
fun openFaqPage()
fun openEmail()
fun openEmail(host: String, lastError: String)
}

@ -20,6 +20,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.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.homework.details.HomeworkDetailsDialog
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.MessageModule
@ -98,6 +99,10 @@ abstract class MainModule {
@ContributesAndroidInjector
abstract fun bindHomeworkFragment(): HomeworkFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindHomeworkDetailsDialog(): HomeworkDetailsDialog
@PerFragment
@ContributesAndroidInjector
abstract fun bindLuckyNumberFragment(): LuckyNumberFragment

@ -33,7 +33,7 @@ class MainPresenter @Inject constructor(
Timber.i("Main view was initialized with $startMenuIndex menu index and $startMenuMoreIndex more index")
}
syncManager.startSyncWorker()
syncManager.startPeriodicSyncWorker()
analytics.logEvent("app_open", "destination" to initMenu?.name)
}

@ -39,6 +39,9 @@ class MessageItem(val message: Message, private val noSubjectString: String) :
text = message.date.toFormattedString()
setTypeface(null, style)
}
with(messageItemAttachmentIcon) {
visibility = if (message.hasAttachments) View.VISIBLE else View.GONE
}
}
}
@ -56,7 +59,8 @@ class MessageItem(val message: Message, private val noSubjectString: String) :
return message.hashCode()
}
class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter), LayoutContainer {
override val containerView: View
get() = contentView
}

@ -0,0 +1,88 @@
package io.github.wulkanowy.ui.modules.message.preview
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.data.repositories.message.MessageFolder
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.synthetic.main.item_message_attachment.view.*
import kotlinx.android.synthetic.main.item_message_preview.view.*
import javax.inject.Inject
class MessagePreviewAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
enum class ViewType(val id: Int) {
MESSAGE(1),
DIVIDER(2),
ATTACHMENT(3)
}
var messageWithAttachment: MessageWithAttachment? = null
set(value) {
field = value
attachments = value?.attachments.orEmpty()
}
private var attachments: List<MessageAttachment> = emptyList()
override fun getItemCount() = if (messageWithAttachment == null) 0 else attachments.size + 1 + if (attachments.isNotEmpty()) 1 else 0
override fun getItemViewType(position: Int) = when (position) {
0 -> ViewType.MESSAGE.id
1 -> ViewType.DIVIDER.id
else -> ViewType.ATTACHMENT.id
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ViewType.MESSAGE.id -> MessageViewHolder(inflater.inflate(R.layout.item_message_preview, parent, false))
ViewType.DIVIDER.id -> DividerViewHolder(inflater.inflate(R.layout.item_message_divider, parent, false))
ViewType.ATTACHMENT.id -> AttachmentViewHolder(inflater.inflate(R.layout.item_message_attachment, parent, false))
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is MessageViewHolder -> bindMessage(holder.view, requireNotNull(messageWithAttachment).message)
is AttachmentViewHolder -> bindAttachment(holder.view, requireNotNull(messageWithAttachment).attachments[position - 2])
}
}
@SuppressLint("SetTextI18n")
private fun bindMessage(view: View, message: Message) {
with(view) {
messagePreviewSubject.text = if (message.subject.isNotBlank()) message.subject else context.getString(R.string.message_no_subject)
messagePreviewDate.text = context.getString(R.string.message_date, message.date.toFormattedString("yyyy-MM-dd HH:mm:ss"))
messagePreviewContent.text = message.content
messagePreviewAuthor.text = if (message.folderId == MessageFolder.SENT.id) "${context.getString(R.string.message_to)} ${message.recipient}"
else "${context.getString(R.string.message_from)} ${message.sender}"
}
}
private fun bindAttachment(view: View, attachment: MessageAttachment) {
with(view) {
messagePreviewAttachment.visibility = View.VISIBLE
messagePreviewAttachment.text = attachment.filename
setOnClickListener {
context.openInternetBrowser(attachment.url) { }
}
}
}
class MessageViewHolder(val view: View) : RecyclerView.ViewHolder(view)
class DividerViewHolder(val view: View) : RecyclerView.ViewHolder(view)
class AttachmentViewHolder(val view: View) : RecyclerView.ViewHolder(view)
}

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.message.preview
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
@ -10,8 +9,10 @@ import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
@ -25,6 +26,9 @@ class MessagePreviewFragment : BaseFragment(), MessagePreviewView, MainView.Titl
@Inject
lateinit var presenter: MessagePreviewPresenter
@Inject
lateinit var previewAdapter: MessagePreviewAdapter
private var menuReplyButton: MenuItem? = null
private var menuForwardButton: MenuItem? = null
@ -34,18 +38,15 @@ class MessagePreviewFragment : BaseFragment(), MessagePreviewView, MainView.Titl
override val titleStringId: Int
get() = R.string.message_title
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"
fun newInstance(messageId: Long): MessagePreviewFragment {
fun newInstance(message: Message): MessagePreviewFragment {
return MessagePreviewFragment().apply {
arguments = Bundle().apply { putLong(MESSAGE_ID_KEY, messageId) }
arguments = Bundle().apply { putSerializable(MESSAGE_ID_KEY, message) }
}
}
}
@ -62,11 +63,16 @@ class MessagePreviewFragment : BaseFragment(), MessagePreviewView, MainView.Titl
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = messagePreviewContainer
presenter.onAttachView(this, (savedInstanceState ?: arguments)?.getLong(MESSAGE_ID_KEY) ?: 0L)
presenter.onAttachView(this, (savedInstanceState ?: arguments)?.getSerializable(MESSAGE_ID_KEY) as Message)
}
override fun initView() {
messagePreviewErrorDetails.setOnClickListener { presenter.onDetailsClick() }
with(messagePreviewRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = previewAdapter
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -86,26 +92,11 @@ class MessagePreviewFragment : BaseFragment(), MessagePreviewView, MainView.Titl
}
}
override fun setSubject(subject: String) {
messagePreviewSubject.text = subject
}
@SuppressLint("SetTextI18n")
override fun setRecipient(recipient: String) {
messagePreviewAuthor.text = "${getString(R.string.message_to)} $recipient"
}
@SuppressLint("SetTextI18n")
override fun setSender(sender: String) {
messagePreviewAuthor.text = "${getString(R.string.message_from)} $sender"
}
override fun setDate(date: String) {
messagePreviewDate.text = getString(R.string.message_date, date)
}
override fun setContent(content: String) {
messagePreviewContent.text = content
override fun setMessageWithAttachment(item: MessageWithAttachment) {
with(previewAdapter) {
messageWithAttachment = item
notifyDataSetChanged()
}
}
override fun showProgress(show: Boolean) {
@ -113,7 +104,7 @@ class MessagePreviewFragment : BaseFragment(), MessagePreviewView, MainView.Titl
}
override fun showContent(show: Boolean) {
messagePreviewContentContainer.visibility = if (show) VISIBLE else GONE
messagePreviewRecycler.visibility = if (show) VISIBLE else GONE
}
override fun showOptions(show: Boolean) {
@ -160,7 +151,7 @@ class MessagePreviewFragment : BaseFragment(), MessagePreviewView, MainView.Titl
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(MESSAGE_ID_KEY, presenter.messageId)
outState.putSerializable(MESSAGE_ID_KEY, presenter.message)
}
override fun onDestroyView() {

@ -1,14 +1,12 @@
package io.github.wulkanowy.ui.modules.message.preview
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.repositories.message.MessageFolder
import io.github.wulkanowy.data.repositories.message.MessageRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.toFormattedString
import timber.log.Timber
import javax.inject.Inject
@ -20,61 +18,51 @@ class MessagePreviewPresenter @Inject constructor(
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository, schedulers) {
var messageId = 0L
private var message: Message? = null
var message: Message? = null
private lateinit var lastError: Throwable
private var retryCallback: () -> Unit = {}
fun onAttachView(view: MessagePreviewView, id: Long) {
fun onAttachView(view: MessagePreviewView, message: Message) {
super.onAttachView(view)
view.initView()
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData(id)
loadData(message)
}
private fun onMessageLoadRetry() {
private fun onMessageLoadRetry(message: Message) {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData(messageId)
loadData(message)
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
private fun loadData(id: Long) {
Timber.i("Loading message $id preview started")
messageId = id
private fun loadData(message: Message) {
Timber.i("Loading message ${message.messageId} preview started")
disposable.apply {
clear()
add(studentRepository.getCurrentStudent()
.flatMap { messageRepository.getMessage(it, messageId, true) }
.flatMap { messageRepository.getMessage(it, message, true) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally { view?.showProgress(false) }
.subscribe({ message ->
Timber.i("Loading message $id preview result: Success ")
this@MessagePreviewPresenter.message = message
view?.run {
message.let {
setSubject(if (it.subject.isNotBlank()) it.subject else noSubjectString)
setDate(it.date.toFormattedString("yyyy-MM-dd HH:mm:ss"))
setContent(it.content)
initOptions()
if (it.folderId == MessageFolder.SENT.id) setRecipient(it.recipient)
else setSender(it.sender)
}
Timber.i("Loading message ${message.message.messageId} preview result: Success ")
this@MessagePreviewPresenter.message = message.message
view?.apply {
setMessageWithAttachment(message)
initOptions()
}
analytics.logEvent("load_message_preview", "length" to message.content.length)
analytics.logEvent("load_message_preview", "length" to message.message.content.length)
}) {
Timber.i("Loading message $id preview result: An exception occurred ")
retryCallback = { onMessageLoadRetry() }
Timber.i("Loading message ${message.messageId} preview result: An exception occurred ")
retryCallback = { onMessageLoadRetry(message) }
errorHandler.dispatch(it)
})
}
@ -119,8 +107,6 @@ class MessagePreviewPresenter @Inject constructor(
}, { error ->
retryCallback = { onMessageDelete() }
errorHandler.dispatch(error)
}, {
view?.showErrorView(true)
})
)
}

@ -1,25 +1,16 @@
package io.github.wulkanowy.ui.modules.message.preview
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.github.wulkanowy.ui.base.BaseView
interface MessagePreviewView : BaseView {
val noSubjectString: String
val deleteMessageSuccessString: String
fun initView()
fun setSubject(subject: String)
fun setRecipient(recipient: String)
fun setSender(sender: String)
fun setDate(date: String)
fun setContent(content: String)
fun setMessageWithAttachment(item: MessageWithAttachment)
fun showProgress(show: Boolean)

@ -12,6 +12,7 @@ import eu.davidea.flexibleadapter.common.FlexibleItemDecoration
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.repositories.message.MessageFolder
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
@ -116,8 +117,8 @@ class MessageTabFragment : BaseFragment(), MessageTabView {
messageTabSwipe.isRefreshing = show
}
override fun openMessage(messageId: Long) {
(activity as? MainActivity)?.pushView(MessagePreviewFragment.newInstance(messageId))
override fun openMessage(message: Message) {
(activity as? MainActivity)?.pushView(MessagePreviewFragment.newInstance(message))
}
override fun notifyParentDataLoaded() {

@ -1,7 +1,6 @@
package io.github.wulkanowy.ui.modules.message.tab
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.repositories.message.MessageFolder
import io.github.wulkanowy.data.repositories.message.MessageRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
@ -63,11 +62,10 @@ class MessageTabPresenter @Inject constructor(
if (item is MessageItem) {
Timber.i("Select message ${item.message.id} item")
view?.run {
openMessage(item.message.id)
openMessage(item.message)
if (item.message.unread) {
item.message.unread = false
updateItem(item)
updateMessage(item.message)
}
}
}
@ -119,16 +117,4 @@ class MessageTabPresenter @Inject constructor(
} else showError(message, error)
}
}
private fun updateMessage(message: Message) {
Timber.i("Attempt to update message ${message.id}")
disposable.add(messageRepository.updateMessage(message)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({ Timber.d("Update message ${message.id} result: Success") })
{ error ->
Timber.i("Update message ${message.id} result: An exception occurred")
errorHandler.dispatch(error)
})
}
}

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.message.tab
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.message.MessageItem
@ -32,7 +33,7 @@ interface MessageTabView : BaseView {
fun showRefresh(show: Boolean)
fun openMessage(messageId: Long)
fun openMessage(message: Message)
fun notifyParentDataLoaded()
}

@ -1,14 +1,18 @@
package io.github.wulkanowy.ui.modules.note
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.synthetic.main.dialog_note.*
import io.github.wulkanowy.sdk.scrapper.notes.Note.CategoryType
class NoteDialog : DialogFragment() {
@ -36,6 +40,7 @@ class NoteDialog : DialogFragment() {
return inflater.inflate(R.layout.dialog_note, container, false)
}
@SuppressLint("SetTextI18n")
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
@ -43,6 +48,16 @@ class NoteDialog : DialogFragment() {
noteDialogCategory.text = note.category
noteDialogTeacher.text = note.teacher
noteDialogContent.text = note.content
if (note.isPointsShow) {
with(noteDialogPoints) {
text = "${if (note.points > 0) "+" else ""}${note.points}"
setTextColor(when (CategoryType.getByValue(note.categoryType)) {
CategoryType.POSITIVE -> ContextCompat.getColor(requireContext(), R.color.note_positive)
CategoryType.NEGATIVE -> ContextCompat.getColor(requireContext(), R.color.note_negative)
else -> requireContext().getThemeAttrColor(android.R.attr.textColorPrimary)
})
}
}
noteDialogClose.setOnClickListener { dismiss() }
}
}

@ -1,14 +1,20 @@
package io.github.wulkanowy.ui.modules.note
import android.annotation.SuppressLint
import android.graphics.Typeface.BOLD
import android.graphics.Typeface.NORMAL
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.core.content.ContextCompat
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.Note
import io.github.wulkanowy.sdk.scrapper.notes.Note.CategoryType
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_note.*
@ -17,20 +23,30 @@ class NoteItem(val note: Note) : AbstractFlexibleItem<NoteItem.ViewHolder>() {
override fun getLayoutRes() = R.layout.item_note
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): NoteItem.ViewHolder {
return NoteItem.ViewHolder(view, adapter)
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter)
}
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: NoteItem.ViewHolder, position: Int, payloads: MutableList<Any>?) {
@SuppressLint("SetTextI18n")
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>?) {
holder.apply {
noteItemDate.apply {
with(noteItemDate) {
text = note.date.toFormattedString()
setTypeface(null, if (note.isRead) NORMAL else BOLD)
}
noteItemType.apply {
with(noteItemType) {
text = note.category
setTypeface(null, if (note.isRead) NORMAL else BOLD)
}
with(noteItemPoints) {
text = "${if (note.points > 0) "+" else ""}${note.points}"
visibility = if (note.isPointsShow) VISIBLE else GONE
setTextColor(when(CategoryType.getByValue(note.categoryType)) {
CategoryType.POSITIVE -> ContextCompat.getColor(context, R.color.note_positive)
CategoryType.NEGATIVE -> ContextCompat.getColor(context, R.color.note_negative)
else -> context.getThemeAttrColor(android.R.attr.textColorPrimary)
})
}
noteItemTeacher.text = note.teacher
noteItemContent.text = note.content
}
@ -53,7 +69,8 @@ class NoteItem(val note: Note) : AbstractFlexibleItem<NoteItem.ViewHolder>() {
return result
}
class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) :
FlexibleViewHolder(view, adapter), LayoutContainer {
override val containerView: View
get() = contentView
}

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.settings
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.yariksoffice.lingver.Lingver
@ -33,11 +34,24 @@ class SettingsFragment : PreferenceFragmentCompat(),
override val titleStringId get() = R.string.settings_title
override val syncSuccessString get() = getString(R.string.pref_services_message_sync_success)
override val syncFailedString get() = getString(R.string.pref_services_message_sync_failed)
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
}
override fun initView() {
findPreference<Preference>(getString(R.string.pref_key_services_force_sync))?.run {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
presenter.onSyncNowClicked()
true
}
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this)
@ -61,12 +75,19 @@ class SettingsFragment : PreferenceFragmentCompat(),
}
override fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean) {
findPreference<Preference>(serviceEnablesKey)?.apply {
findPreference<Preference>(serviceEnablesKey)?.run {
summary = if (isHolidays) getString(R.string.pref_services_suspended) else ""
isEnabled = !isHolidays
}
}
override fun setSyncInProgress(inProgress: Boolean) {
findPreference<Preference>(getString(R.string.pref_key_services_force_sync))?.run {
isEnabled = !inProgress
summary = if (inProgress) getString(R.string.pref_services_sync_in_progress) else ""
}
}
override fun showError(text: String, error: Throwable) {
(activity as? BaseActivity<*>)?.showError(text, error)
}
@ -87,6 +108,15 @@ class SettingsFragment : PreferenceFragmentCompat(),
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
override fun showForceSyncDialog() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.pref_services_dialog_force_sync_title)
.setMessage(R.string.pref_services_dialog_force_sync_summary)
.setPositiveButton(android.R.string.ok) { _, _ -> presenter.onForceSyncDialogSubmit() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.settings
import com.readystatesoftware.chuck.api.ChuckCollector
import androidx.work.WorkInfo
import com.chuckerteam.chucker.api.ChuckerCollector
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager
@ -21,7 +22,7 @@ class SettingsPresenter @Inject constructor(
private val preferencesRepository: PreferencesRepository,
private val analytics: FirebaseAnalyticsHelper,
private val syncManager: SyncManager,
private val chuckCollector: ChuckCollector,
private val chuckerCollector: ChuckerCollector,
private val appInfo: AppInfo
) : BasePresenter<SettingsView>(errorHandler, studentRepository, schedulers) {
@ -29,6 +30,7 @@ class SettingsPresenter @Inject constructor(
super.onAttachView(view)
Timber.i("Settings view was initialized")
view.setServicesSuspended(preferencesRepository.serviceEnableKey, now().isHolidays)
view.initView()
}
fun onSharedPreferenceChanged(key: String) {
@ -36,9 +38,9 @@ class SettingsPresenter @Inject constructor(
with(preferencesRepository) {
when (key) {
serviceEnableKey -> with(syncManager) { if (isServiceEnabled) startSyncWorker() else stopSyncWorker() }
servicesIntervalKey, servicesOnlyWifiKey -> syncManager.startSyncWorker(true)
isDebugNotificationEnableKey -> chuckCollector.showNotification(isDebugNotificationEnable)
serviceEnableKey -> with(syncManager) { if (isServiceEnabled) startPeriodicSyncWorker() else stopSyncWorker() }
servicesIntervalKey, servicesOnlyWifiKey -> syncManager.startPeriodicSyncWorker(true)
isDebugNotificationEnableKey -> chuckerCollector.showNotification = isDebugNotificationEnable
appThemeKey -> view?.recreateView()
appLanguageKey -> view?.run {
updateLanguage(if (appLanguage == "system") appInfo.systemLanguage else appLanguage)
@ -49,4 +51,25 @@ class SettingsPresenter @Inject constructor(
}
analytics.logEvent("setting_changed", "name" to key)
}
fun onSyncNowClicked() {
view?.showForceSyncDialog()
}
fun onForceSyncDialogSubmit() {
view?.run {
Timber.i("Setting sync now started")
analytics.logEvent("sync_now_started")
disposable.add(syncManager.startOneTimeSyncWorker()
.doOnSubscribe { setSyncInProgress(true) }
.doFinally { setSyncInProgress(false) }
.subscribe({ workInfo ->
if (workInfo.state == WorkInfo.State.SUCCEEDED) showMessage(syncSuccessString)
else if (workInfo.state == WorkInfo.State.FAILED) showError(syncFailedString, Throwable(workInfo.outputData.getString("error")))
}, {
Timber.e("Sync now failed")
})
)
}
}
}

@ -4,9 +4,19 @@ import io.github.wulkanowy.ui.base.BaseView
interface SettingsView : BaseView {
val syncSuccessString: String
val syncFailedString: String
fun initView()
fun recreateView()
fun updateLanguage(langCode: String)
fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean)
fun setSyncInProgress(inProgress: Boolean)
fun showForceSyncDialog()
}

@ -1,15 +1,15 @@
package io.github.wulkanowy.ui.modules.timetable.completed
import android.content.res.Resources
import com.readystatesoftware.chuck.api.ChuckCollector
import com.chuckerteam.chucker.api.ChuckerCollector
import io.github.wulkanowy.sdk.exception.FeatureDisabledException
import io.github.wulkanowy.ui.base.ErrorHandler
import javax.inject.Inject
class CompletedLessonsErrorHandler @Inject constructor(
resources: Resources,
chuckCollector: ChuckCollector
) : ErrorHandler(resources, chuckCollector) {
chuckerCollector: ChuckerCollector
) : ErrorHandler(resources, chuckerCollector) {
var onFeatureDisabled: () -> Unit = {}

@ -32,12 +32,16 @@ fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -
}
}
fun Context.openEmailClient(chooserTitle: String, email: String, subject: String?, body: String?) {
val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", email, null))
emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
if (subject != null) emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject)
if (body != null) emailIntent.putExtra(Intent.EXTRA_TEXT, body)
startActivity(Intent.createChooser(emailIntent, chooserTitle))
fun Context.openEmailClient(chooserTitle: String, email: String, subject: String, body: String, onActivityNotFound: () -> Unit = {}) {
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")).apply {
putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, body)
}
if (intent.resolveActivity(packageManager) != null) {
startActivity(Intent.createChooser(intent, chooserTitle))
} else onActivityNotFound()
}
fun Context.openNavigation(location: String) {

@ -1,10 +1,11 @@
Wersja 0.16.0
Wersja 0.17.0
- oceny zapisane w nawiasach nie są już liczone do średniej
- naprawiliśmy wyświetlanie szczęśliwych numerków na kontach rodziców (może być potrzebne zalogowanie się ponownie)
- dodaliśmy opcję przywracania hasła, ulepszyliśmy formularz logowania
- dodaliśmy język ukraiński i niemiecki
- dodaliśmy informację na górnym pasku o bieżącym semestrze w widoku ocen
- ulepszyliśmy przełączanie aplikacji na nowy semestr
- dodaliśmy wsparcie dla załączników w wiadomosciach i zadaniach domowych
- dodaliśmy oznaczanie zadań domowych jako wykonanych
- dodaliśmy wyświetlanie punktów przy uwagach
- dodaliśmy funkcję powiadomień o awariach dziennika
- dodaliśmy funkcję wymuszenia pełnej synchronizacji
- zmieniliśmy sposób zwracania się do użytkownika na bezosobowy
- naprawiliśmy logowanie na androidach niższych niż 5.0
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

Binary file not shown.

After

(image error) Size: 724 B

Binary file not shown.

After

(image error) Size: 470 B

Binary file not shown.

After

(image error) Size: 955 B

Binary file not shown.

After

(image error) Size: 1.4 KiB

Binary file not shown.

After

(image error) Size: 2.0 KiB

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M2,12.5C2,9.46 4.46,7 7.5,7H18c2.21,0 4,1.79 4,4s-1.79,4 -4,4H9.5C8.12,15 7,13.88 7,12.5S8.12,10 9.5,10H17v2H9.41c-0.55,0 -0.55,1 0,1H18c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2H7.5C5.57,9 4,10.57 4,12.5S5.57,16 7.5,16H17v2H7.5C4.46,18 2,15.54 2,12.5z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -97,13 +98,46 @@
android:textIsSelectable="true"
android:textSize="12sp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/homeworkDialogClose"
style="@style/Widget.MaterialComponents.Button.TextButton"
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="15dp"
android:text="@string/all_close" />
android:layout_marginTop="10dp"
android:text="@string/homework_attachments"
android:textSize="17sp" />
<TextView
android:id="@+id/homeworkDialogAttachments"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:lineSpacingMultiplier="1.2"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="14sp" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/homeworkDialogRead"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/homework_mark_as_done"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/homeworkDialogClose"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="1dp"
android:text="@string/all_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

@ -64,6 +64,22 @@
android:textIsSelectable="true"
android:textSize="12sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/note_points"
android:textSize="17sp" />
<TextView
android:id="@+id/noteDialogPoints"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="12sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

@ -108,31 +108,41 @@
android:text="@string/login_header_symbol"
android:textSize="16sp"
app:fontFamily="sans-serif-light"
app:layout_constraintBottom_toTopOf="@+id/loginSymbolNameLayout"
app:layout_constraintBottom_toTopOf="@+id/loginSymbolHelper"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSymbolContact"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/loginSymbolHelper"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:gravity="center_horizontal"
android:text="@string/login_symbol_helper"
app:fontFamily="sans-serif-light"
app:layout_constraintBottom_toTopOf="@id/loginSymbolNameLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginSymbolHeader"
app:layout_constraintVertical_chainStyle="packed" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/loginSymbolNameLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginTop="48dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:layout_margin="24dp"
android:hint="@string/login_symbol_hint"
app:helperText="@string/login_symbol_helper"
app:helperTextEnabled="true"
app:layout_constraintBottom_toTopOf="@+id/loginSymbolSignIn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSymbolHeader">
app:layout_constraintTop_toBottomOf="@+id/loginSymbolHelper">
<AutoCompleteTextView
android:id="@+id/loginSymbolName"

@ -5,55 +5,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/messagePreviewRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/messagePreviewContentContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/messagePreviewSubject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:lineSpacingMultiplier="1.2"
android:textSize="22sp"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/messagePreviewAuthor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:textColor="?android:textColorSecondary"
android:textSize="15sp"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/messagePreviewDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:textColor="?android:textColorSecondary"
android:textSize="15sp"
tools:text="@tools:sample/date/ddmmyy" />
<TextView
android:id="@+id/messagePreviewContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:lineSpacingMultiplier="1.2"
android:textIsSelectable="true"
tools:text="@tools:sample/lorem" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
android:layout_height="match_parent"
tools:itemCount="1"
tools:listitem="@layout/item_message_preview" />
<LinearLayout
android:id="@+id/messagePreviewError"
@ -95,7 +52,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/all_details" />
<com.google.android.material.button.MaterialButton

@ -1,4 +1,5 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/homework_subitem_container"
android:layout_width="match_parent"
@ -8,43 +9,75 @@
<TextView
android:id="@+id/homeworkItemSubject"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginLeft="15dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="5dp"
android:textSize="17sp"
tools:text="@tools:sample/lorem" />
android:ellipsize="end"
android:singleLine="true"
android:textSize="16sp"
app:layout_constraintEnd_toStartOf="@+id/homeworkItemTeacher"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/homeworkItemTeacher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="15dp"
android:layout_marginRight="15dp"
android:layout_toEndOf="@id/homeworkItemSubject"
android:layout_toRightOf="@id/homeworkItemSubject"
android:ellipsize="end"
android:gravity="end"
android:maxWidth="200dp"
android:minWidth="80dp"
android:singleLine="true"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/homeworkItemSubject"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/homeworkItemContent"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/homeworkItemSubject"
android:layout_alignStart="@id/homeworkItemSubject"
android:layout_alignLeft="@id/homeworkItemSubject"
android:layout_marginEnd="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="50dp"
android:layout_marginBottom="15dp"
android:ellipsize="end"
android:lineSpacingMultiplier="1.2"
android:maxLines="2"
android:textSize="14sp"
tools:text="@tools:sample/lorem" />
</RelativeLayout>
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/homeworkItemSubject"
app:layout_constraintTop_toBottomOf="@id/homeworkItemSubject"
tools:text="@tools:sample/lorem/random" />
<ImageView
android:id="@+id/homeworkItemCheckImage"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="@id/homeworkItemTeacher"
app:layout_constraintTop_toBottomOf="@id/homeworkItemTeacher"
app:srcCompat="@drawable/ic_check"
app:tint="?android:textColorSecondary"
tools:ignore="ContentDescription"
tools:visibility="gone" />
<ImageView
android:id="@+id/homeworkItemAttachmentImage"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="@id/homeworkItemTeacher"
app:layout_constraintTop_toBottomOf="@id/homeworkItemTeacher"
app:srcCompat="@drawable/ic_attachment"
app:tint="?android:textColorSecondary"
tools:ignore="ContentDescription"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -14,9 +14,11 @@
android:id="@+id/licenseItemName"
android:layout_width="match_parent"
android:layout_height="28dp"
android:ellipsize="end"
android:gravity="bottom"
android:singleLine="true"
android:textSize="16sp"
tools:text="@tools:sample/lorem" />
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/licenseItemSummary"

@ -1,5 +1,7 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
@ -11,41 +13,53 @@
<TextView
android:id="@+id/messageItemAuthor"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginEnd="40dp"
android:layout_marginRight="40dp"
android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
android:textSize="15sp"
tools:text="@tools:sample/full_names" />
app:layout_constraintEnd_toStartOf="@+id/messageItemDate"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/messageItemDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_toEndOf="@id/messageItemAuthor"
android:layout_toRightOf="@id/messageItemAuthor"
android:gravity="end"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/date/ddmmyy" />
<TextView
android:id="@+id/messageItemSubject"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@+id/messageItemAuthor"
android:layout_alignStart="@id/messageItemAuthor"
android:layout_alignLeft="@id/messageItemAuthor"
android:layout_marginTop="5dp"
android:layout_marginEnd="10dp"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toStartOf="@id/messageItemAttachmentIcon"
app:layout_constraintStart_toStartOf="@id/messageItemAuthor"
app:layout_constraintTop_toBottomOf="@+id/messageItemAuthor"
app:layout_goneMarginEnd="0dp"
tools:text="@tools:sample/lorem/random" />
</RelativeLayout>
<ImageView
android:id="@+id/messageItemAttachmentIcon"
android:layout_width="16dp"
android:layout_height="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/messageItemSubject"
app:layout_constraintEnd_toEndOf="@id/messageItemDate"
app:srcCompat="@drawable/ic_attachment"
app:tint="?colorOnBackground"
tools:ignore="ContentDescription"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,22 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:paddingStart="16dp"
android:paddingTop="10dp"
android:paddingEnd="16dp"
android:paddingBottom="10dp">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/messagePreviewAttachment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_attachment"
app:drawableTint="?colorOnBackground"
tools:text="@tools:sample/lorem"
tools:visibility="visible" />
</LinearLayout>

@ -0,0 +1,6 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:background="?android:attr/listDivider" />

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/messagePreviewContentContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/messagePreviewSubject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:lineSpacingMultiplier="1.2"
android:textSize="22sp"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/messagePreviewAuthor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:textColor="?android:textColorSecondary"
android:textSize="15sp"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/messagePreviewDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:textColor="?android:textColorSecondary"
android:textSize="15sp"
tools:text="@tools:sample/date/ddmmyy" />
<TextView
android:id="@+id/messagePreviewContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:lineSpacingMultiplier="1.2"
android:textIsSelectable="true"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>

@ -1,4 +1,5 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/note_subitem_container"
android:layout_width="match_parent"
@ -8,54 +9,76 @@
<TextView
android:id="@+id/noteItemDate"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginLeft="15dp"
android:layout_marginTop="10dp"
android:textColor="?android:textColorSecondary"
android:textSize="15sp"
app:layout_constraintRight_toLeftOf="@+id/noteItemTeacher"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/date/ddmmyy" />
<TextView
android:id="@+id/noteItemTeacher"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="15dp"
android:layout_marginRight="15dp"
android:layout_toEndOf="@id/noteItemDate"
android:layout_toRightOf="@id/noteItemDate"
android:ellipsize="end"
android:gravity="end"
android:singleLine="true"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/noteItemDate"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/noteItemType"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/noteItemDate"
android:layout_alignStart="@id/noteItemDate"
android:layout_alignLeft="@id/noteItemDate"
android:layout_marginTop="5dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="5dp"
android:textSize="13sp"
android:ellipsize="end"
android:maxLines="2"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="@id/noteItemDate"
app:layout_constraintRight_toLeftOf="@id/noteItemPoints"
app:layout_constraintTop_toBottomOf="@id/noteItemDate"
app:layout_goneMarginEnd="0dp"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/noteItemContent"
android:layout_width="wrap_content"
android:id="@+id/noteItemPoints"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/noteItemType"
android:layout_alignStart="@id/noteItemDate"
android:layout_alignLeft="@id/noteItemDate"
android:layout_marginEnd="15dp"
android:layout_marginRight="15dp"
android:layout_marginTop="8dp"
android:textSize="16sp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintRight_toRightOf="@id/noteItemTeacher"
app:layout_constraintTop_toBottomOf="@id/noteItemTeacher"
tools:text="-5"
tools:textColor="@color/note_positive"
tools:visibility="visible" />
<TextView
android:id="@+id/noteItemContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="15dp"
android:ellipsize="end"
android:lineSpacingMultiplier="1.2"
android:maxLines="2"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="@id/noteItemDate"
app:layout_constraintRight_toRightOf="@+id/noteItemTeacher"
app:layout_constraintTop_toBottomOf="@id/noteItemType"
tools:text="@tools:sample/lorem/random" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -3,7 +3,7 @@
<item
android:id="@+id/logViewerMenuShare"
android:icon="@drawable/chuck_ic_share_white_24dp"
android:icon="@drawable/ic_share"
android:orderInCategory="1"
android:title="@string/logviewer_share"
app:iconTint="@color/material_on_surface_emphasis_medium"

@ -0,0 +1 @@
io_github_wulkanowy__sdk:apache_2_0

@ -0,0 +1,3 @@
com_github_wulkanowy__material_chips_input:2019
io_github_wulkanowy__uonet_request_signer:2019
io_github_wulkanowy__sdk:2020

@ -0,0 +1,3 @@
com_github_wulkanowy__material_chips_input:Material Chips Input
io_github_wulkanowy__uonet_request_signer:UONET+ Request Signer
io_github_wulkanowy__sdk:VULCAN UONET+ SDK

@ -48,11 +48,12 @@
<string name="login_invalid_token">Ungültige token</string>
<string name="login_expired_token">Token ist nicht mehr gültig</string>
<string name="login_invalid_email">Ungültige email</string>
<string name="login_invalid_login">Ungültige login</string>
<string name="login_invalid_symbol">Ungültige symbol</string>
<string name="login_incorrect_symbol">Student nicht gefunden. Überprüfen Sie das Symbol</string>
<string name="login_field_required">Dieses Datenfeld ist erforderlich</string>
<string name="login_duplicate_student">Ausgewählter Student ist bereits angemeldet.</string>
<string name="login_symbol_helper">Das Symbol finden Sie auf der Registerseite unter Uczeń -> Dostęp Mobilny -> Zarejestruj urządzenie mobilne</string>
<string name="login_symbol_helper">Das Symbol finden Sie auf der Registerseite unter <b>Uczeń</b> →&#160;<b>Dostęp Mobilny</b>&#160;<b>Zarejestruj urządzenie mobilne</b></string>
<string name="login_select_student">Wählen Sie die Studenten aus, die sich bei der Anwendung anmelden sollen.</string>
<string name="login_advanced">Andere Optionen</string>
<string name="login_privacy_policy">Datenschutzerklärung</string>
@ -201,6 +202,7 @@
<!--Note-->
<string name="note_no_items">Keine Informationen über Eintragen</string>
<string name="note_points">Punkte</string>
<plurals name="note_number_item">
<item quantity="one">%d Eintrag</item>
<item quantity="other">%d Eintragen</item>
@ -217,6 +219,9 @@
<!--Homework-->
<string name="homework_no_items">Keine Informationen über Hausaufgaben</string>
<string name="homework_mark_as_done">Gemacht</string>
<string name="homework_mark_as_undone">Unvollständig</string>
<string name="homework_attachments">Anhänge</string>
<!--Lucky number-->
@ -354,6 +359,7 @@
<string name="channel_lucky_number">Glückliche Nummer</string>
<string name="channel_new_message">Neue Nachrichten</string>
<string name="channel_new_notes">Neue Eintragen</string>
<string name="channel_push">Push-Benachrichtigungen</string>
<string name="channel_debug">Debuggen</string>

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