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

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

View File

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

View File

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

View File

@ -43,3 +43,10 @@
#Config for Material Components #Config for Material Components
-keep class com.google.android.material.tabs.** { *; } -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

View File

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

View File

@ -22,7 +22,8 @@
<activity <activity
android:name=".ui.modules.splash.SplashActivity" android:name=".ui.modules.splash.SplashActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/WulkanowyTheme.SplashScreen"> android:theme="@style/WulkanowyTheme.SplashScreen"
tools:ignore="LockedOrientationActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@ -112,5 +113,13 @@
<meta-data <meta-data
android:name="firebase_crashlytics_collection_enabled" android:name="firebase_crashlytics_collection_enabled"
android:value="${crashlytics_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> </application>
</manifest> </manifest>

View File

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

View File

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

View File

@ -48,4 +48,14 @@ class Converters {
fun gsonToIntList(value: String): List<Int> { fun gsonToIntList(value: String): List<Int> {
return Gson().fromJson(value, object : TypeToken<List<Int>>() {}.type) 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)
}
} }

View File

@ -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>
}

View File

@ -2,19 +2,22 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageWithAttachment
import io.reactivex.Maybe import io.reactivex.Maybe
import io.reactivex.Single import io.reactivex.Single
@Dao @Dao
interface MessagesDao : BaseDao<Message> { 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") @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>> 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") @Query("SELECT * FROM Messages WHERE student_id = :studentId AND removed = 1 ORDER BY date DESC")
fun loadDeleted(studentId: Int): Maybe<List<Message>> fun loadDeleted(studentId: Int): Maybe<List<Message>>
} }

View File

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

View File

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

View File

@ -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

View File

@ -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>
)

View File

@ -16,8 +16,19 @@ data class Note(
val teacher: String, val teacher: String,
@ColumnInfo(name = "teacher_symbol")
val teacherSymbol: String,
val category: 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 val content: String
) : Serializable { ) : Serializable {

View File

@ -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")
}
}

View File

@ -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)
)
""")
}
}

View File

@ -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 \"[]\"")
}
}

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.utils.friday import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.monday import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Completable
import io.reactivex.Single import io.reactivex.Single
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import java.net.UnknownHostException import java.net.UnknownHostException
@ -36,4 +37,12 @@ class HomeworkRepository @Inject constructor(
}.flatMap { local.getHomework(semester, monday, friday).toSingle(emptyList()) }) }.flatMap { local.getHomework(semester, monday, friday).toSingle(emptyList()) })
} }
} }
fun toggleDone(homework: Homework): Completable {
return Completable.fromCallable {
local.updateHomework(listOf(homework.apply {
isDone = !isDone
}))
}
}
} }

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.SdkHelper import io.github.wulkanowy.data.SdkHelper
import io.github.wulkanowy.data.db.entities.Message 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.Recipient
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
@ -11,7 +12,6 @@ import io.github.wulkanowy.data.repositories.message.MessageFolder.RECEIVED
import io.github.wulkanowy.sdk.pojo.SentMessage import io.github.wulkanowy.sdk.pojo.SentMessage
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Maybe
import io.reactivex.Single import io.reactivex.Single
import timber.log.Timber import timber.log.Timber
import java.net.UnknownHostException 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)) return Single.just(sdkHelper.init(student))
.flatMap { _ -> .flatMap { _ ->
local.getMessage(messageDbId) local.getMessageWithAttachment(student, message)
.filter { .filter {
it.content.isNotEmpty().also { status -> it.message.content.isNotEmpty().also { status ->
Timber.d("Message content in db empty: ${!status}") Timber.d("Message content in db empty: ${!status}")
} } && !it.message.unread
} }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap { .flatMap {
if (it) local.getMessage(messageDbId) if (it) local.getMessageWithAttachment(student, message)
else Single.error(UnknownHostException()) else Single.error(UnknownHostException())
} }
.flatMap { dbMessage -> .flatMap { dbMessage ->
remote.getMessagesContent(dbMessage, markAsRead).doOnSuccess { remote.getMessagesContentDetails(dbMessage.message, markAsRead).doOnSuccess { (downloadedMessage, attachments) ->
local.updateMessages(listOf(dbMessage.copy(unread = !markAsRead).apply { local.updateMessages(listOf(dbMessage.message.copy(unread = !markAsRead).apply {
id = dbMessage.id id = dbMessage.message.id
content = content.ifBlank { it } 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 { }.flatMap {
local.getMessage(messageDbId) local.getMessageWithAttachment(student, message)
} }
) )
} }
@ -83,10 +84,6 @@ class MessageRepository @Inject constructor(
.toSingle(emptyList()) .toSingle(emptyList())
} }
fun updateMessage(message: Message): Completable {
return Completable.fromCallable { local.updateMessages(listOf(message)) }
}
fun updateMessages(messages: List<Message>): Completable { fun updateMessages(messages: List<Message>): Completable {
return Completable.fromCallable { local.updateMessages(messages) } 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) return ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap { .flatMap {
if (it) remote.deleteMessage(message) if (it) remote.deleteMessage(message)
else Single.error(UnknownHostException()) else Single.error(UnknownHostException())
} }
.filter { it }
.doOnSuccess { .doOnSuccess {
if (!message.removed) local.updateMessages(listOf(message.copy(removed = true).apply { if (!message.removed) local.updateMessages(listOf(message.copy(removed = true).apply {
id = message.id id = message.id

View File

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

View File

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

View File

@ -5,18 +5,24 @@ import android.os.Build.VERSION_CODES.O
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.work.BackoffPolicy.EXPONENTIAL import androidx.work.BackoffPolicy.EXPONENTIAL
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy.KEEP import androidx.work.ExistingPeriodicWorkPolicy.KEEP
import androidx.work.ExistingPeriodicWorkPolicy.REPLACE import androidx.work.ExistingPeriodicWorkPolicy.REPLACE
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType.CONNECTED import androidx.work.NetworkType.CONNECTED
import androidx.work.NetworkType.UNMETERED import androidx.work.NetworkType.UNMETERED
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import com.paulinasadowska.rxworkmanagerobservers.extensions.getWorkInfoByIdObservable
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.SharedPrefProvider.Companion.APP_VERSION_CODE_KEY import io.github.wulkanowy.data.db.SharedPrefProvider.Companion.APP_VERSION_CODE_KEY
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.services.sync.channels.Channel import io.github.wulkanowy.services.sync.channels.Channel
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.isHolidays import io.github.wulkanowy.utils.isHolidays
import io.reactivex.Observable
import org.threeten.bp.LocalDate.now import org.threeten.bp.LocalDate.now
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit.MINUTES 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()) { 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) sharedPrefProvider.putLong(APP_VERSION_CODE_KEY, appInfo.versionCode.toLong(), true)
} }
Timber.i("SyncManager was initialized") Timber.i("SyncManager was initialized")
} }
fun startSyncWorker(restart: Boolean = false) { fun startPeriodicSyncWorker(restart: Boolean = false) {
if (preferencesRepository.isServiceEnabled && !now().isHolidays) { if (preferencesRepository.isServiceEnabled && !now().isHolidays) {
workManager.enqueueUniquePeriodicWork(SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP, workManager.enqueueUniquePeriodicWork(SyncWorker::class.java.simpleName, if (restart) REPLACE else KEEP,
PeriodicWorkRequestBuilder<SyncWorker>(preferencesRepository.servicesInterval, MINUTES) 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() { fun stopSyncWorker() {
workManager.cancelUniqueWork(SyncWorker::class.java.simpleName) workManager.cancelUniqueWork(SyncWorker::class.java.simpleName)
} }

View File

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

View File

@ -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
}
)
}
}

View File

@ -1,7 +1,7 @@
package io.github.wulkanowy.ui.base package io.github.wulkanowy.ui.base
import android.content.res.Resources 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.R
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.sdk.exception.BadCredentialsException import io.github.wulkanowy.sdk.exception.BadCredentialsException
@ -15,7 +15,7 @@ import java.net.SocketTimeoutException
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject 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 = { _, _ -> } var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> }
@ -24,7 +24,7 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources,
var onNoCurrentStudent: () -> Unit = {} var onNoCurrentStudent: () -> Unit = {}
fun dispatch(error: Throwable) { 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") Timber.e(error, "An exception occurred while the Wulkanowy was running")
proceed(error) proceed(error)
} }

View File

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

View File

@ -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>) { override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
with(holder) { with(holder) {
licenseItemName.text = library.libraryName licenseItemName.text = library.libraryName
licenseItemSummary.text = library.license?.licenseName licenseItemSummary.text = library.license?.licenseName?.takeIf { it.isNotBlank() } ?: library.libraryVersion
} }
} }

View File

@ -27,10 +27,6 @@ class LicensePresenter @Inject constructor(
private fun loadData() { private fun loadData() {
disposable.add(Single.fromCallable { view?.appLibraries } 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) } } .map { it.map { library -> LicenseItem(library) } }
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)

View File

@ -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() }
}
}

View File

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

View File

@ -24,6 +24,8 @@ class HomeworkItem(header: HomeworkHeader, val homework: Homework) :
homeworkItemSubject.text = homework.subject homeworkItemSubject.text = homework.subject
homeworkItemTeacher.text = homework.teacher homeworkItemTeacher.text = homework.teacher
homeworkItemContent.text = homework.content 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 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 override val containerView: View
get() = contentView get() = contentView
} }

View File

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

View File

@ -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()
}
}

View File

@ -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)
}
)
}
}

View File

@ -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)
}

View File

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

View File

@ -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) { override fun setErrorPassRequired(focus: Boolean) {
with(loginFormPassLayout) { with(loginFormPassLayout) {
if (focus) requestFocus() if (focus) requestFocus()

View File

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

View File

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

View File

@ -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) { override fun setErrorSymbolRequired(focus: Boolean) {
with(loginFormSymbolLayout) { with(loginFormSymbolLayout) {
if (focus) requestFocus() if (focus) requestFocus()
@ -219,12 +233,18 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
} }
} }
override fun openEmail() { override fun openEmail(lastError: String) {
context?.openEmailClient( context?.openEmailClient(
requireContext().getString(R.string.login_email_intent_title), chooserTitle = requireContext().getString(R.string.login_email_intent_title),
"wulkanowyinc@gmail.com", email = "wulkanowyinc@gmail.com",
requireContext().getString(R.string.login_email_subject), subject = requireContext().getString(R.string.login_email_subject),
requireContext().getString(R.string.login_email_text, appInfo.systemModel, appInfo.systemVersion.toString(), appInfo.versionName) body = requireContext().getString(R.string.login_email_text,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
appInfo.versionName,
"$formHostValue/$formSymbolValue",
lastError
)
) )
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,6 +39,9 @@ class MessageItem(val message: Message, private val noSubjectString: String) :
text = message.date.toFormattedString() text = message.date.toFormattedString()
setTypeface(null, style) 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() 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 override val containerView: View
get() = contentView get() = contentView
} }

View File

@ -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)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
package io.github.wulkanowy.ui.modules.message.tab package io.github.wulkanowy.ui.modules.message.tab
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem 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.MessageFolder
import io.github.wulkanowy.data.repositories.message.MessageRepository import io.github.wulkanowy.data.repositories.message.MessageRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository import io.github.wulkanowy.data.repositories.semester.SemesterRepository
@ -63,11 +62,10 @@ class MessageTabPresenter @Inject constructor(
if (item is MessageItem) { if (item is MessageItem) {
Timber.i("Select message ${item.message.id} item") Timber.i("Select message ${item.message.id} item")
view?.run { view?.run {
openMessage(item.message.id) openMessage(item.message)
if (item.message.unread) { if (item.message.unread) {
item.message.unread = false item.message.unread = false
updateItem(item) updateItem(item)
updateMessage(item.message)
} }
} }
} }
@ -119,16 +117,4 @@ class MessageTabPresenter @Inject constructor(
} else showError(message, error) } 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)
})
}
} }

View File

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

View File

@ -1,14 +1,18 @@
package io.github.wulkanowy.ui.modules.note package io.github.wulkanowy.ui.modules.note
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.synthetic.main.dialog_note.* import kotlinx.android.synthetic.main.dialog_note.*
import io.github.wulkanowy.sdk.scrapper.notes.Note.CategoryType
class NoteDialog : DialogFragment() { class NoteDialog : DialogFragment() {
@ -36,6 +40,7 @@ class NoteDialog : DialogFragment() {
return inflater.inflate(R.layout.dialog_note, container, false) return inflater.inflate(R.layout.dialog_note, container, false)
} }
@SuppressLint("SetTextI18n")
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
@ -43,6 +48,16 @@ class NoteDialog : DialogFragment() {
noteDialogCategory.text = note.category noteDialogCategory.text = note.category
noteDialogTeacher.text = note.teacher noteDialogTeacher.text = note.teacher
noteDialogContent.text = note.content 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() } noteDialogClose.setOnClickListener { dismiss() }
} }
} }

View File

@ -1,14 +1,20 @@
package io.github.wulkanowy.ui.modules.note package io.github.wulkanowy.ui.modules.note
import android.annotation.SuppressLint
import android.graphics.Typeface.BOLD import android.graphics.Typeface.BOLD
import android.graphics.Typeface.NORMAL import android.graphics.Typeface.NORMAL
import android.view.View 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.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note 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 io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_note.* 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 getLayoutRes() = R.layout.item_note
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): NoteItem.ViewHolder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return NoteItem.ViewHolder(view, adapter) 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 { holder.apply {
noteItemDate.apply { with(noteItemDate) {
text = note.date.toFormattedString() text = note.date.toFormattedString()
setTypeface(null, if (note.isRead) NORMAL else BOLD) setTypeface(null, if (note.isRead) NORMAL else BOLD)
} }
noteItemType.apply { with(noteItemType) {
text = note.category text = note.category
setTypeface(null, if (note.isRead) NORMAL else BOLD) 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 noteItemTeacher.text = note.teacher
noteItemContent.text = note.content noteItemContent.text = note.content
} }
@ -53,7 +69,8 @@ class NoteItem(val note: Note) : AbstractFlexibleItem<NoteItem.ViewHolder>() {
return result 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 override val containerView: View
get() = contentView get() = contentView
} }

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.settings
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.yariksoffice.lingver.Lingver import com.yariksoffice.lingver.Lingver
@ -33,11 +34,24 @@ class SettingsFragment : PreferenceFragmentCompat(),
override val titleStringId get() = R.string.settings_title 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) { override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this) AndroidSupportInjection.inject(this)
super.onAttach(context) 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?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this) presenter.onAttachView(this)
@ -61,12 +75,19 @@ class SettingsFragment : PreferenceFragmentCompat(),
} }
override fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean) { 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 "" summary = if (isHolidays) getString(R.string.pref_services_suspended) else ""
isEnabled = !isHolidays 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) { override fun showError(text: String, error: Throwable) {
(activity as? BaseActivity<*>)?.showError(text, error) (activity as? BaseActivity<*>)?.showError(text, error)
} }
@ -87,6 +108,15 @@ class SettingsFragment : PreferenceFragmentCompat(),
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString()) 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() { override fun onResume() {
super.onResume() super.onResume()
preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this) preferenceScreen.sharedPreferences.registerOnSharedPreferenceChangeListener(this)

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.settings 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.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager import io.github.wulkanowy.services.sync.SyncManager
@ -21,7 +22,7 @@ class SettingsPresenter @Inject constructor(
private val preferencesRepository: PreferencesRepository, private val preferencesRepository: PreferencesRepository,
private val analytics: FirebaseAnalyticsHelper, private val analytics: FirebaseAnalyticsHelper,
private val syncManager: SyncManager, private val syncManager: SyncManager,
private val chuckCollector: ChuckCollector, private val chuckerCollector: ChuckerCollector,
private val appInfo: AppInfo private val appInfo: AppInfo
) : BasePresenter<SettingsView>(errorHandler, studentRepository, schedulers) { ) : BasePresenter<SettingsView>(errorHandler, studentRepository, schedulers) {
@ -29,6 +30,7 @@ class SettingsPresenter @Inject constructor(
super.onAttachView(view) super.onAttachView(view)
Timber.i("Settings view was initialized") Timber.i("Settings view was initialized")
view.setServicesSuspended(preferencesRepository.serviceEnableKey, now().isHolidays) view.setServicesSuspended(preferencesRepository.serviceEnableKey, now().isHolidays)
view.initView()
} }
fun onSharedPreferenceChanged(key: String) { fun onSharedPreferenceChanged(key: String) {
@ -36,9 +38,9 @@ class SettingsPresenter @Inject constructor(
with(preferencesRepository) { with(preferencesRepository) {
when (key) { when (key) {
serviceEnableKey -> with(syncManager) { if (isServiceEnabled) startSyncWorker() else stopSyncWorker() } serviceEnableKey -> with(syncManager) { if (isServiceEnabled) startPeriodicSyncWorker() else stopSyncWorker() }
servicesIntervalKey, servicesOnlyWifiKey -> syncManager.startSyncWorker(true) servicesIntervalKey, servicesOnlyWifiKey -> syncManager.startPeriodicSyncWorker(true)
isDebugNotificationEnableKey -> chuckCollector.showNotification(isDebugNotificationEnable) isDebugNotificationEnableKey -> chuckerCollector.showNotification = isDebugNotificationEnable
appThemeKey -> view?.recreateView() appThemeKey -> view?.recreateView()
appLanguageKey -> view?.run { appLanguageKey -> view?.run {
updateLanguage(if (appLanguage == "system") appInfo.systemLanguage else appLanguage) updateLanguage(if (appLanguage == "system") appInfo.systemLanguage else appLanguage)
@ -49,4 +51,25 @@ class SettingsPresenter @Inject constructor(
} }
analytics.logEvent("setting_changed", "name" to key) 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")
})
)
}
}
} }

View File

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

View File

@ -1,15 +1,15 @@
package io.github.wulkanowy.ui.modules.timetable.completed package io.github.wulkanowy.ui.modules.timetable.completed
import android.content.res.Resources 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.sdk.exception.FeatureDisabledException
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import javax.inject.Inject import javax.inject.Inject
class CompletedLessonsErrorHandler @Inject constructor( class CompletedLessonsErrorHandler @Inject constructor(
resources: Resources, resources: Resources,
chuckCollector: ChuckCollector chuckerCollector: ChuckerCollector
) : ErrorHandler(resources, chuckCollector) { ) : ErrorHandler(resources, chuckerCollector) {
var onFeatureDisabled: () -> Unit = {} var onFeatureDisabled: () -> Unit = {}

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" <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_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -97,13 +98,46 @@
android:textIsSelectable="true" android:textIsSelectable="true"
android:textSize="12sp" /> android:textSize="12sp" />
<com.google.android.material.button.MaterialButton <TextView
android:id="@+id/homeworkDialogClose"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_marginTop="10dp"
android:layout_marginTop="15dp" android:text="@string/homework_attachments"
android:text="@string/all_close" /> 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> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

View File

@ -64,6 +64,22 @@
android:textIsSelectable="true" android:textIsSelectable="true"
android:textSize="12sp" /> 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 <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

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

View File

@ -5,55 +5,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
tools:itemCount="1"
<LinearLayout tools:listitem="@layout/item_message_preview" />
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>
<LinearLayout <LinearLayout
android:id="@+id/messagePreviewError" android:id="@+id/messagePreviewError"
@ -95,7 +52,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/all_details" /> android:text="@string/all_details" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton

View File

@ -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" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/homework_subitem_container" android:id="@+id/homework_subitem_container"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -8,43 +9,75 @@
<TextView <TextView
android:id="@+id/homeworkItemSubject" android:id="@+id/homeworkItemSubject"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="15dp" android:layout_marginStart="15dp"
android:layout_marginLeft="15dp"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:layout_marginBottom="5dp" android:layout_marginBottom="5dp"
android:textSize="17sp" android:ellipsize="end"
tools:text="@tools:sample/lorem" /> 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 <TextView
android:id="@+id/homeworkItemTeacher" android:id="@+id/homeworkItemTeacher"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:layout_marginEnd="15dp" android:layout_marginEnd="15dp"
android:layout_marginRight="15dp" android:ellipsize="end"
android:layout_toEndOf="@id/homeworkItemSubject"
android:layout_toRightOf="@id/homeworkItemSubject"
android:gravity="end" android:gravity="end"
android:maxWidth="200dp"
android:minWidth="80dp"
android:singleLine="true"
android:textSize="13sp" 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" /> tools:text="@tools:sample/full_names" />
<TextView <TextView
android:id="@+id/homeworkItemContent" android:id="@+id/homeworkItemContent"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/homeworkItemSubject" android:layout_marginTop="5dp"
android:layout_alignStart="@id/homeworkItemSubject" android:layout_marginEnd="50dp"
android:layout_alignLeft="@id/homeworkItemSubject"
android:layout_marginEnd="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="15dp" android:layout_marginBottom="15dp"
android:ellipsize="end"
android:lineSpacingMultiplier="1.2" android:lineSpacingMultiplier="1.2"
android:maxLines="2"
android:textSize="14sp" android:textSize="14sp"
tools:text="@tools:sample/lorem" /> app:layout_constraintBottom_toBottomOf="parent"
</RelativeLayout> 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>

View File

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

View File

@ -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" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/relativeLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
@ -11,41 +13,53 @@
<TextView <TextView
android:id="@+id/messageItemAuthor" android:id="@+id/messageItemAuthor"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentTop="true" android:layout_marginEnd="10dp"
android:layout_marginEnd="40dp" android:layout_marginRight="10dp"
android:layout_marginRight="40dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:singleLine="true"
android:textSize="15sp" 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 <TextView
android:id="@+id/messageItemDate" android:id="@+id/messageItemDate"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="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:gravity="end"
android:textSize="13sp" android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/date/ddmmyy" /> tools:text="@tools:sample/date/ddmmyy" />
<TextView <TextView
android:id="@+id/messageItemSubject" android:id="@+id/messageItemSubject"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" 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_marginTop="5dp"
android:layout_marginEnd="10dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:singleLine="true"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="12sp" 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" /> 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>

View File

@ -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>

View File

@ -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" />

View File

@ -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>

View File

@ -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" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/note_subitem_container" android:id="@+id/note_subitem_container"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -8,54 +9,76 @@
<TextView <TextView
android:id="@+id/noteItemDate" android:id="@+id/noteItemDate"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="15dp" android:layout_marginStart="15dp"
android:layout_marginLeft="15dp"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:textColor="?android:textColorSecondary"
android:textSize="15sp" 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" /> tools:text="@tools:sample/date/ddmmyy" />
<TextView <TextView
android:id="@+id/noteItemTeacher" android:id="@+id/noteItemTeacher"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:layout_marginEnd="15dp" android:layout_marginEnd="15dp"
android:layout_marginRight="15dp" android:ellipsize="end"
android:layout_toEndOf="@id/noteItemDate"
android:layout_toRightOf="@id/noteItemDate"
android:gravity="end" android:gravity="end"
android:singleLine="true"
android:textSize="13sp" 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" /> tools:text="@tools:sample/full_names" />
<TextView <TextView
android:id="@+id/noteItemType" android:id="@+id/noteItemType"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" 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_marginTop="5dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="5dp" 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" /> tools:text="@tools:sample/lorem" />
<TextView <TextView
android:id="@+id/noteItemContent" android:id="@+id/noteItemPoints"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/noteItemType" android:layout_marginTop="8dp"
android:layout_alignStart="@id/noteItemDate" android:textSize="16sp"
android:layout_alignLeft="@id/noteItemDate" android:textStyle="bold"
android:layout_marginEnd="15dp" android:visibility="gone"
android:layout_marginRight="15dp" 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:layout_marginBottom="15dp"
android:ellipsize="end"
android:lineSpacingMultiplier="1.2" android:lineSpacingMultiplier="1.2"
android:maxLines="2"
android:textSize="14sp" 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" /> tools:text="@tools:sample/lorem/random" />
</RelativeLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -48,11 +48,12 @@
<string name="login_invalid_token">Ungültige token</string> <string name="login_invalid_token">Ungültige token</string>
<string name="login_expired_token">Token ist nicht mehr gültig</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_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_invalid_symbol">Ungültige symbol</string>
<string name="login_incorrect_symbol">Student nicht gefunden. Überprüfen Sie das 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_field_required">Dieses Datenfeld ist erforderlich</string>
<string name="login_duplicate_student">Ausgewählter Student ist bereits angemeldet.</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_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_advanced">Andere Optionen</string>
<string name="login_privacy_policy">Datenschutzerklärung</string> <string name="login_privacy_policy">Datenschutzerklärung</string>
@ -201,6 +202,7 @@
<!--Note--> <!--Note-->
<string name="note_no_items">Keine Informationen über Eintragen</string> <string name="note_no_items">Keine Informationen über Eintragen</string>
<string name="note_points">Punkte</string>
<plurals name="note_number_item"> <plurals name="note_number_item">
<item quantity="one">%d Eintrag</item> <item quantity="one">%d Eintrag</item>
<item quantity="other">%d Eintragen</item> <item quantity="other">%d Eintragen</item>
@ -217,6 +219,9 @@
<!--Homework--> <!--Homework-->
<string name="homework_no_items">Keine Informationen über Hausaufgaben</string> <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--> <!--Lucky number-->
@ -354,6 +359,7 @@
<string name="channel_lucky_number">Glückliche Nummer</string> <string name="channel_lucky_number">Glückliche Nummer</string>
<string name="channel_new_message">Neue Nachrichten</string> <string name="channel_new_message">Neue Nachrichten</string>
<string name="channel_new_notes">Neue Eintragen</string> <string name="channel_new_notes">Neue Eintragen</string>
<string name="channel_push">Push-Benachrichtigungen</string>
<string name="channel_debug">Debuggen</string> <string name="channel_debug">Debuggen</string>

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