1
0

Compare commits

...

21 Commits
0.7.6 ... 0.8.0

Author SHA1 Message Date
df0a1e59cc Version 0.8.0 2019-04-29 20:39:35 +02:00
dbbc8069b1 Add showing proper fragment in notifications (#331) 2019-04-29 19:56:23 +02:00
f84040109c Add lucky number widget (#292) 2019-04-29 14:33:33 +02:00
baf1420193 Improve login student selection layout (#329) 2019-04-29 00:55:30 +02:00
4a36d78709 Add activity and fragment lifecycle logging (#327) 2019-04-26 23:53:02 +02:00
4464812651 Fix configure activity's theme (#325) 2019-04-22 09:27:25 +02:00
72a35481e5 Fix showing last message after update (#326) 2019-04-22 09:13:26 +02:00
017c200115 Fix grade summary final grade string (#324) 2019-04-20 22:19:06 +02:00
2bf7755157 Add AMOLED mode (#279) 2019-04-19 23:52:34 +02:00
269af4b7ba Update dependencies (#323) 2019-04-18 16:38:49 +02:00
7431738366 Add a selection of multiple students to login (#318) 2019-04-18 12:18:58 +02:00
034b99c7ab Add counting of the full-year average to the summary of grades (#322) 2019-04-18 00:32:43 +02:00
74e98e4430 Change style of privacy policy link (#321) 2019-04-09 23:33:53 +02:00
cbf3215dd1 Merge branch '0.7.x' 2019-04-08 13:51:02 +02:00
c18877466f Add account picker for timetable widget (#314)
Close #281
2019-04-08 00:18:45 +02:00
aa6dcaff94 Merge branch '0.7.x' 2019-04-06 01:18:03 +02:00
2bff468e56 Add deleting messages (#290) 2019-03-31 22:01:04 +02:00
f2855d598d Merge branch '0.7.x' 2019-03-30 20:19:34 +01:00
1ebc296bfe Merge branch '0.7.x' 2019-03-26 21:22:17 +01:00
a2a18e5652 Merge branch '0.7.x' 2019-03-24 23:22:44 +01:00
cd1ceea860 Add messages forwarding (#288) 2019-03-21 22:55:47 +01:00
126 changed files with 2062 additions and 533 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -59,4 +59,8 @@ class MessageRemote @Inject constructor(private val api: Api) {
}
)
}
fun deleteMessage(message: Message): Single<Boolean> {
return api.deleteMessages(listOf(Pair(message.realId, message.folderId)))
}
}

View File

@ -9,6 +9,7 @@ import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.message.MessageFolder.RECEIVED
import io.reactivex.Completable
import io.reactivex.Maybe
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
@ -89,4 +90,20 @@ class MessageRepository @Inject constructor(
else Single.error(UnknownHostException())
}
}
fun deleteMessage(message: Message): Maybe<Boolean> {
return ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.deleteMessage(message)
else Single.error(UnknownHostException())
}
.filter { it }
.doOnSuccess {
if (!message.removed) local.updateMessages(listOf(message.copy(removed = true).apply {
id = message.id
content = message.content
}))
else local.deleteMessages(listOf(message))
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,18 +2,36 @@ package io.github.wulkanowy.ui.base
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import dagger.android.support.DaggerAppCompatActivity
import dagger.android.AndroidInjection
import dagger.android.DispatchingAndroidInjector
import dagger.android.support.HasSupportFragmentInjector
import io.github.wulkanowy.R
import io.github.wulkanowy.utils.FragmentLifecycleLogger
import javax.inject.Inject
abstract class BaseActivity : DaggerAppCompatActivity(), BaseView {
abstract class BaseActivity : AppCompatActivity(), BaseView, HasSupportFragmentInjector {
@Inject
lateinit var supportFragmentInjector: DispatchingAndroidInjector<Fragment>
@Inject
lateinit var fragmentLifecycleLogger: FragmentLifecycleLogger
@Inject
lateinit var themeManager: ThemeManager
protected lateinit var messageContainer: View
public override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
themeManager.applyTheme(this)
super.onCreate(savedInstanceState)
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
}
@ -31,4 +49,6 @@ abstract class BaseActivity : DaggerAppCompatActivity(), BaseView {
super.onDestroy()
invalidateOptionsMenu()
}
override fun supportFragmentInjector() = supportFragmentInjector
}

View File

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

View File

@ -44,7 +44,7 @@ class AboutFragment : BaseFragment(), AboutView, MainView.TitledView {
.withFields(R.string::class.java.fields)
.withCheckCachedDetection(false)
.withExcludedLibraries("fastadapter", "AndroidIconics", "Jsoup", "Retrofit", "okio",
"OkHttp", "Butterknife", "CircleImageView")
"Butterknife", "CircleImageView")
.withOnExtraListener { presenter.onExtraSelect(it) })
}.let {
fragmentCompat.onCreateView(inflater.context, inflater, container, savedInstanceState, it)

View File

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

View File

@ -19,8 +19,8 @@ class AccountPresenter @Inject constructor(
override fun onAttachView(view: AccountView) {
super.onAttachView(view)
Timber.i("Account dialog is attached")
view.initView()
Timber.i("Account dialog view was initialized")
loadData()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
package io.github.wulkanowy.ui.modules.login.form
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
@ -54,8 +54,8 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
loginFormName.setOnTextChangedListener { presenter.onNameTextChanged() }
loginFormPass.setOnTextChangedListener { presenter.onPassTextChanged() }
loginFormHost.setOnItemSelectedListener { presenter.onHostSelected() }
loginFormPrivacyPolicyLink.movementMethod = LinkMovementMethod.getInstance()
loginFormSignIn.setOnClickListener { presenter.attemptLogin() }
loginFormSignIn.setOnClickListener { presenter.onSignInClick() }
loginFormPrivacyLink.setOnClickListener { presenter.onPrivacyLinkClick() }
loginFormPass.setOnEditorActionListener { _, id, _ ->
if (id == IME_ACTION_DONE || id == IME_NULL) loginFormSignIn.callOnClick() else false
@ -132,6 +132,10 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
}
}
override fun showPrivacyPolicy() {
loginFormPrivacyLink.visibility = VISIBLE
}
override fun notifyParentAccountLogged(students: List<Student>) {
(activity as? LoginActivity)?.onFormFragmentAccountLogged(students, Triple(
loginFormName.text.toString(),
@ -140,6 +144,10 @@ class LoginFormFragment : BaseFragment(), LoginFormView {
))
}
override fun openPrivacyPolicyPage() {
startActivity(Intent.parseUri("https://wulkanowy.github.io/polityka-prywatnosci.html", 0))
}
override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView()

View File

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

View File

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

View File

@ -8,7 +8,6 @@ import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
@ -45,6 +44,7 @@ class LoginStudentSelectFragment : BaseFragment(), LoginStudentSelectView {
}
override fun initView() {
loginStudentSelectSignIn.setOnClickListener { presenter.onSignIn() }
loginAdapter.apply { setOnItemClickListener { presenter.onItemSelected(it) } }
loginStudentSelectRecycler.apply {
@ -54,7 +54,7 @@ class LoginStudentSelectFragment : BaseFragment(), LoginStudentSelectView {
}
override fun updateData(data: List<LoginStudentSelectItem>) {
loginAdapter.updateDataSet(data, true)
loginAdapter.updateDataSet(data)
}
override fun openMainView() {
@ -69,11 +69,11 @@ class LoginStudentSelectFragment : BaseFragment(), LoginStudentSelectView {
}
override fun showContent(show: Boolean) {
loginStudentSelectRecycler.visibility = if (show) VISIBLE else GONE
loginStudentSelectContent.visibility = if (show) VISIBLE else GONE
}
override fun showActionBar(show: Boolean) {
(activity as? AppCompatActivity)?.supportActionBar?.run { if (show) show() else hide() }
override fun enableSignIn(enable: Boolean) {
loginStudentSelectSignIn.isEnabled = enable
}
fun onParentInitStudentSelectFragment(students: List<Student>) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,8 +22,12 @@ import io.github.wulkanowy.ui.modules.account.AccountDialog
import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.safelyPopFragment
@ -40,7 +44,7 @@ class MainActivity : BaseActivity(), MainView {
lateinit var navController: FragNavController
companion object {
const val EXTRA_START_MENU_INDEX = "extraStartMenuIndex"
const val EXTRA_START_MENU = "extraStartMenu"
fun getStartIntent(context: Context) = Intent(context, MainActivity::class.java)
}
@ -56,14 +60,27 @@ class MainActivity : BaseActivity(), MainView {
override var startMenuIndex = 0
override var startMenuMoreIndex = -1
private val moreMenuFragments = listOf<Fragment>(
MessageFragment.newInstance(),
HomeworkFragment.newInstance(),
NoteFragment.newInstance(),
LuckyNumberFragment.newInstance()
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(mainToolbar)
messageContainer = mainFragmentContainer
presenter.onAttachView(this, intent.getIntExtra(EXTRA_START_MENU_INDEX, -1))
navController.initialize(startMenuIndex, savedInstanceState)
presenter.onAttachView(this, intent.getSerializableExtra(EXTRA_START_MENU) as? MainView.MenuView)
navController.run {
initialize(startMenuIndex, savedInstanceState)
pushFragment(moreMenuFragments.getOrNull(startMenuMoreIndex))
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
@ -168,7 +185,7 @@ class MainActivity : BaseActivity(), MainView {
.apply { addFlags(FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK) })
}
override fun onSaveInstanceState(outState: Bundle?) {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
navController.onSaveInstanceState(outState)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ class MessagePreviewPresenter @Inject constructor(
var messageId: Int = 0
private var replyMessage: Message? = null
private var message: Message? = null
fun onAttachView(view: MessagePreviewView, id: Int) {
super.onAttachView(view)
@ -41,13 +41,13 @@ class MessagePreviewPresenter @Inject constructor(
.doFinally { view?.showProgress(false) }
.subscribe({ message ->
Timber.i("Loading message $id preview result: Success ")
replyMessage = message
this@MessagePreviewPresenter.message = message
view?.run {
message.let {
setSubject(if (it.subject.isNotBlank()) it.subject else noSubjectString)
setDate(it.date.toFormattedString("yyyy-MM-dd HH:mm:ss"))
setContent(it.content.orEmpty())
showReplyButton(true)
initOptions()
if (it.recipient.isNotBlank()) setRecipient(it.recipient)
else setSender(it.sender)
@ -63,13 +63,69 @@ class MessagePreviewPresenter @Inject constructor(
}
fun onReply(): Boolean {
return if (replyMessage != null) {
view?.openMessageReply(replyMessage)
return if (message != null) {
view?.openMessageReply(message)
true
} else false
}
fun onForward(): Boolean {
return if (message != null) {
view?.openMessageForward(message)
true
} else false
}
private fun deleteMessage() {
message?.let { message ->
disposable.add(messageRepository.deleteMessage(message)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doOnSubscribe {
view?.run {
showContent(false)
showProgress(true)
showOptions(false)
}
}
.doFinally {
view?.showProgress(false)
}
.subscribe({
view?.run {
notifyParentMessageDeleted(message)
showMessage(deleteMessageSuccessString)
popView()
}
}, { error ->
view?.showMessageError()
errorHandler.dispatch(error)
}, {
view?.showMessageError()
})
)
}
}
fun onMessageDelete(): Boolean {
deleteMessage()
return true
}
private fun initOptions() {
view?.apply {
showOptions(message != null)
message?.let {
when (it.removed) {
true -> setDeletedOptionsLabels()
false -> setNotDeletedOptionsLabels()
}
}
}
}
fun onCreateOptionsMenu() {
view?.showReplyButton(replyMessage != null)
initOptions()
}
}

View File

@ -7,6 +7,8 @@ interface MessagePreviewView : BaseSessionView {
val noSubjectString: String
val deleteMessageSuccessString: String
fun setSubject(subject: String)
fun setRecipient(recipient: String)
@ -19,9 +21,21 @@ interface MessagePreviewView : BaseSessionView {
fun showProgress(show: Boolean)
fun showReplyButton(show: Boolean)
fun showContent(show: Boolean)
fun showOptions(show: Boolean)
fun setDeletedOptionsLabels()
fun setNotDeletedOptionsLabels()
fun showMessageError()
fun openMessageReply(message: Message?)
fun openMessageForward(message: Message?)
fun popView()
fun notifyParentMessageDeleted(message: Message)
}

View File

@ -26,11 +26,14 @@ class SendMessageActivity : BaseActivity(), SendMessageView {
companion object {
private const val EXTRA_MESSAGE = "EXTRA_MESSAGE"
private const val EXTRA_REPLY = "EXTRA_REPLY"
fun getStartIntent(context: Context) = Intent(context, SendMessageActivity::class.java)
fun getStartIntent(context: Context, message: Message?): Intent {
return getStartIntent(context).putExtra(EXTRA_MESSAGE, message)
fun getStartIntent(context: Context, message: Message?, reply: Boolean = false): Intent {
return getStartIntent(context)
.putExtra(EXTRA_MESSAGE, message)
.putExtra(EXTRA_REPLY, reply)
}
}
@ -59,7 +62,7 @@ class SendMessageActivity : BaseActivity(), SendMessageView {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
messageContainer = sendMessageContainer
presenter.onAttachView(this, intent.getSerializableExtra(EXTRA_MESSAGE) as? Message)
presenter.onAttachView(this, intent.getSerializableExtra(EXTRA_MESSAGE) as? Message, intent.getSerializableExtra(EXTRA_REPLY) as? Boolean)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {

View File

@ -30,18 +30,25 @@ class SendMessagePresenter @Inject constructor(
private val analytics: FirebaseAnalyticsHelper
) : BasePresenter<SendMessageView>(errorHandler) {
fun onAttachView(view: SendMessageView, message: Message?) {
fun onAttachView(view: SendMessageView, message: Message?, reply: Boolean?) {
super.onAttachView(view)
Timber.i("Send message view is attached")
loadData(message)
Timber.i("Send message view was initialized")
loadData(message, reply)
view.apply {
message?.let {
setSubject("RE: ${message.subject}")
if (preferencesRepository.fillMessageContent) {
setContent(when (message.sender.isNotEmpty()) {
true -> "\n\nOd: ${message.sender}\n"
false -> "\n\nDo: ${message.recipient}\n"
} + "Data: ${message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${message.content}")
setSubject(when (reply) {
true -> "RE: "
else -> "FW: "
} + message.subject)
if (preferencesRepository.fillMessageContent || reply != true) {
setContent(
when (reply) {
true -> "\n\n"
else -> ""
} + when (message.sender.isNotEmpty()) {
true -> "Od: ${message.sender}\n"
false -> "Do: ${message.recipient}\n"
} + "Data: ${message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${message.content}")
}
}
}
@ -52,7 +59,7 @@ class SendMessagePresenter @Inject constructor(
return true
}
private fun loadData(message: Message?) {
private fun loadData(message: Message?, reply: Boolean?) {
var reportingUnit: ReportingUnit? = null
var recipients: List<Recipient> = emptyList()
var selectedRecipient: List<Recipient> = emptyList()
@ -69,7 +76,7 @@ class SendMessagePresenter @Inject constructor(
recipients = it
}
.flatMapCompletable {
if (message == null) Completable.complete()
if (message == null || reply != true) Completable.complete()
else recipientRepository.getMessageRecipients(student, message)
.doOnSuccess {
Timber.i("Loaded message recipients to reply result: Success, fetched %d recipients", it.size)

View File

@ -17,13 +17,12 @@ import io.github.wulkanowy.ui.base.session.BaseSessionFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.MessageItem
import io.github.wulkanowy.ui.modules.message.MessageView
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_message_tab.*
import javax.inject.Inject
class MessageTabFragment : BaseSessionFragment(), MessageTabView, MessageView.MessageChildView {
class MessageTabFragment : BaseSessionFragment(), MessageTabView {
@Inject
lateinit var presenter: MessageTabPresenter
@ -76,7 +75,7 @@ class MessageTabFragment : BaseSessionFragment(), MessageTabView, MessageView.Me
}
override fun updateData(data: List<MessageItem>) {
tabAdapter.updateDataSet(data, true)
tabAdapter.updateDataSet(data)
}
override fun updateItem(item: AbstractFlexibleItem<*>) {
@ -115,10 +114,14 @@ class MessageTabFragment : BaseSessionFragment(), MessageTabView, MessageView.Me
(parentFragment as? MessageFragment)?.onChildFragmentLoaded()
}
override fun onParentLoadData(forceRefresh: Boolean) {
fun onParentLoadData(forceRefresh: Boolean) {
presenter.onParentViewLoadData(forceRefresh)
}
fun onParentDeleteMessage() {
presenter.onDeleteMessage()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(MessageTabFragment.MESSAGE_TAB_FOLDER_ID, presenter.folder.name)

View File

@ -34,7 +34,29 @@ class MessageTabPresenter @Inject constructor(
onParentViewLoadData(true)
}
fun onDeleteMessage() {
loadData(false)
}
fun onParentViewLoadData(forceRefresh: Boolean) {
loadData(forceRefresh)
}
fun onMessageItemSelected(item: AbstractFlexibleItem<*>) {
if (item is MessageItem) {
Timber.i("Select message ${item.message.realId} item")
view?.run {
openMessage(item.message.realId)
if (item.message.unread) {
item.message.unread = false
updateItem(item)
updateMessage(item.message)
}
}
}
}
private fun loadData(forceRefresh: Boolean) {
Timber.i("Loading $folder message data started")
disposable.apply {
clear()
@ -67,20 +89,6 @@ class MessageTabPresenter @Inject constructor(
}
}
fun onMessageItemSelected(item: AbstractFlexibleItem<*>) {
if (item is MessageItem) {
Timber.i("Select message ${item.message.realId} item")
view?.run {
openMessage(item.message.realId)
if (item.message.unread) {
item.message.unread = false
updateItem(item)
updateMessage(item.message)
}
}
}
}
private fun updateMessage(message: Message) {
Timber.i("Attempt to update message ${message.realId}")
disposable.add(messageRepository.updateMessage(message)

View File

@ -10,8 +10,8 @@ class MorePresenter @Inject constructor(errorHandler: ErrorHandler) : BasePresen
override fun onAttachView(view: MoreView) {
super.onAttachView(view)
Timber.i("More view is attached")
view.initView()
Timber.i("More view was initialized")
loadData()
}

View File

@ -23,8 +23,8 @@ class NotePresenter @Inject constructor(
override fun onAttachView(view: NoteView) {
super.onAttachView(view)
Timber.i("Note view is attached")
view.initView()
Timber.i("Note view was initialized")
loadData()
}

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.settings
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate
import com.takisoft.preferencex.PreferenceFragmentCompat
import dagger.android.support.AndroidSupportInjection
import io.github.wulkanowy.BuildConfig.DEBUG
@ -44,8 +43,7 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
presenter.onSharedPreferenceChanged(key)
}
override fun setTheme(theme: Int) {
AppCompatDelegate.setDefaultNightMode(theme)
override fun recreateView() {
activity?.recreate()
}

View File

@ -21,7 +21,7 @@ class SettingsPresenter @Inject constructor(
override fun onAttachView(view: SettingsView) {
super.onAttachView(view)
Timber.i("Settings view is attached")
Timber.i("Settings view was initialized")
view.setServicesSuspended(preferencesRepository.serviceEnableKey, now().isHolidays)
}
@ -31,8 +31,8 @@ class SettingsPresenter @Inject constructor(
when (key) {
serviceEnableKey -> syncManager.run { if (isServiceEnabled) startSyncWorker() else stopSyncWorker() }
servicesIntervalKey, servicesOnlyWifiKey -> syncManager.startSyncWorker(true)
currentThemeKey -> view?.setTheme(currentTheme)
isDebugNotificationEnableKey -> chuckCollector.showNotification(isDebugNotificationEnable)
appThemeKey -> view?.recreateView()
}
}
analytics.logEvent("setting_changed", "name" to key)

View File

@ -4,7 +4,7 @@ import io.github.wulkanowy.ui.base.BaseView
interface SettingsView : BaseView {
fun setTheme(theme: Int)
fun recreateView()
fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean)
}

View File

@ -80,12 +80,12 @@ class TimetableFragment : BaseSessionFragment(), TimetableView, MainView.MainChi
timetableNextButton.setOnClickListener { presenter.onNextDay() }
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.action_menu_timetable, menu)
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.action_menu_timetable, menu)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
return if (item?.itemId == R.id.timetableMenuCompletedLessons) presenter.onCompletedLessonsSwitchSelected()
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.timetableMenuCompletedLessons) presenter.onCompletedLessonsSwitchSelected()
else false
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,65 @@
package io.github.wulkanowy.ui.modules.timetablewidget
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.db.SharedPrefHelper
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey
import io.github.wulkanowy.utils.SchedulersProvider
import javax.inject.Inject
class TimetableWidgetConfigurePresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val schedulers: SchedulersProvider,
private val studentRepository: StudentRepository,
private val sharedPref: SharedPrefHelper
) : BasePresenter<TimetableWidgetConfigureView>(errorHandler) {
private var appWidgetId: Int? = null
private var isFromProvider = false
fun onAttachView(view: TimetableWidgetConfigureView, appWidgetId: Int?, isFromProvider: Boolean?) {
super.onAttachView(view)
this.appWidgetId = appWidgetId
this.isFromProvider = isFromProvider ?: false
view.initView()
loadData()
}
fun onItemSelect(item: AbstractFlexibleItem<*>) {
if (item is TimetableWidgetConfigureItem) {
registerStudent(item.student)
}
}
private fun loadData() {
disposable.add(studentRepository.getSavedStudents(false)
.map { it to appWidgetId?.let { id -> sharedPref.getLong(getStudentWidgetKey(id), 0) } }
.map { (students, currentStudentId) ->
students.map { student -> TimetableWidgetConfigureItem(student, student.id == currentStudentId) }
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
when {
it.isEmpty() -> view?.openLoginView()
it.size == 1 && !isFromProvider -> registerStudent(it.single().student)
else -> view?.updateData(it)
}
}, { errorHandler.dispatch(it) }))
}
private fun registerStudent(student: Student) {
appWidgetId?.also {
sharedPref.putLong(getStudentWidgetKey(it), student.id)
view?.apply {
updateTimetableWidget(it)
setSuccessResult(it)
}
}
view?.finishView()
}
}

View File

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

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.widgets.timetable
package io.github.wulkanowy.ui.modules.timetablewidget
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
import android.content.Context
import android.content.Intent
import android.graphics.Paint.ANTI_ALIAS_FLAG
@ -15,9 +16,11 @@ import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.data.repositories.timetable.TimetableRepository
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getDateWidgetKey
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.getStudentWidgetKey
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.toFormattedString
import io.reactivex.Single
import io.reactivex.Maybe
import org.threeten.bp.LocalDate
import timber.log.Timber
@ -48,28 +51,37 @@ class TimetableWidgetFactory(
override fun onDestroy() {}
override fun onDataSetChanged() {
intent?.action?.let { LocalDate.ofEpochDay(sharedPref.getLong(it, 0)) }
?.let { date ->
try {
lessons = studentRepository.isStudentSaved()
.flatMap { isSaved ->
if (isSaved) {
studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getCurrentSemester(it) }
.flatMap { timetableRepository.getTimetable(it, date, date) }
} else Single.just(emptyList())
}
.map { item -> item.sortedBy { it.number } }
.subscribeOn(schedulers.backgroundThread)
.blockingGet()
} catch (e: Exception) {
Timber.e(e, "An error has occurred while downloading data for the widget")
}
intent?.extras?.getInt(EXTRA_APPWIDGET_ID)?.let { appWidgetId ->
val date = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(appWidgetId), 0))
val studentId = sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0)
lessons = try {
studentRepository.isStudentSaved()
.filter { true }
.flatMap { studentRepository.getSavedStudents().toMaybe() }
.flatMap {
if (studentId == 0L) throw IllegalArgumentException("Student id is 0")
it.singleOrNull { student -> student.id == studentId }
.let { student ->
if (student != null) Maybe.just(student)
else Maybe.empty()
}
}
.flatMap { semesterRepository.getCurrentSemester(it).toMaybe() }
.flatMap { timetableRepository.getTimetable(it, date, date).toMaybe() }
.map { item -> item.sortedBy { it.number } }
.subscribeOn(schedulers.backgroundThread)
.blockingGet(emptyList())
} catch (e: Exception) {
Timber.e(e, "An error has occurred in timetable widget factory")
emptyList()
}
}
}
override fun getViewAt(position: Int): RemoteViews? {
if (position == INVALID_POSITION || lessons.getOrNull(position) === null) return null
if (position == INVALID_POSITION || lessons.getOrNull(position) == null) return null
return RemoteViews(context.packageName, R.layout.item_widget_timetable).apply {
lessons[position].let {

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.ui.widgets.timetable
package io.github.wulkanowy.ui.modules.timetablewidget
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
@ -10,21 +10,29 @@ import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.widget.RemoteViews
import dagger.android.AndroidInjection
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.SharedPrefHelper
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.services.widgets.TimetableWidgetService
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainActivity.Companion.EXTRA_START_MENU_INDEX
import io.github.wulkanowy.ui.modules.main.MainActivity.Companion.EXTRA_START_MENU
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.shortcutWeekDayName
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.weekDayName
import io.reactivex.Maybe
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDate.now
import timber.log.Timber
import javax.inject.Inject
class TimetableWidgetProvider : BroadcastReceiver() {
@ -32,13 +40,21 @@ class TimetableWidgetProvider : BroadcastReceiver() {
@Inject
lateinit var appWidgetManager: AppWidgetManager
@Inject
lateinit var studentRepository: StudentRepository
@Inject
lateinit var sharedPref: SharedPrefHelper
@Inject
lateinit var schedulers: SchedulersProvider
@Inject
lateinit var analytics: FirebaseAnalyticsHelper
companion object {
const val EXTRA_FROM_PROVIDER = "extraFromProvider"
const val EXTRA_TOGGLED_WIDGET_ID = "extraToggledWidget"
const val EXTRA_BUTTON_TYPE = "extraButtonType"
@ -49,7 +65,9 @@ class TimetableWidgetProvider : BroadcastReceiver() {
const val BUTTON_RESET = "buttonReset"
fun createWidgetKey(appWidgetId: Int) = "timetable_widget_$appWidgetId"
fun getDateWidgetKey(appWidgetId: Int) = "timetable_widget_date_$appWidgetId"
fun getStudentWidgetKey(appWidgetId: Int) = "timetable_widget_student_$appWidgetId"
}
override fun onReceive(context: Context, intent: Intent) {
@ -63,12 +81,14 @@ class TimetableWidgetProvider : BroadcastReceiver() {
private fun onUpdate(context: Context, intent: Intent) {
if (intent.getStringExtra(EXTRA_BUTTON_TYPE) === null) {
intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS)?.forEach { appWidgetId ->
updateWidget(context, appWidgetId, now().nextOrSameSchoolDay)
val student = getStudent(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId)
updateWidget(context, appWidgetId, now().nextOrSameSchoolDay, student)
}
} else {
val buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE)
val toggledWidgetId = intent.getIntExtra(EXTRA_TOGGLED_WIDGET_ID, 0)
val savedDate = LocalDate.ofEpochDay(sharedPref.getLong(createWidgetKey(toggledWidgetId), 0))
val student = getStudent(sharedPref.getLong(getStudentWidgetKey(toggledWidgetId), 0), toggledWidgetId)
val savedDate = LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(toggledWidgetId), 0))
val date = when (buttonType) {
BUTTON_RESET -> now().nextOrSameSchoolDay
BUTTON_NEXT -> savedDate.nextSchoolDay
@ -76,35 +96,46 @@ class TimetableWidgetProvider : BroadcastReceiver() {
else -> now().nextOrSameSchoolDay
}
if (!buttonType.isNullOrBlank()) analytics.logEvent("changed_timetable_widget_day", "button" to buttonType)
updateWidget(context, toggledWidgetId, date)
updateWidget(context, toggledWidgetId, date, student)
}
}
private fun onDelete(intent: Intent) {
intent.getIntExtra(EXTRA_APPWIDGET_ID, 0).let {
if (it != 0) sharedPref.delete(createWidgetKey(it))
if (it != 0) {
sharedPref.apply {
delete(getStudentWidgetKey(it))
delete(getDateWidgetKey(it))
}
}
}
}
private fun updateWidget(context: Context, appWidgetId: Int, date: LocalDate) {
private fun updateWidget(context: Context, appWidgetId: Int, date: LocalDate, student: Student?) {
RemoteViews(context.packageName, R.layout.widget_timetable).apply {
setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty)
setTextViewText(R.id.timetableWidgetDay, date.weekDayName.capitalize())
setTextViewText(R.id.timetableWidgetDate, date.toFormattedString())
setTextViewText(R.id.timetableWidgetDate, "${date.shortcutWeekDayName.capitalize()} ${date.toFormattedString()}")
setTextViewText(R.id.timetableWidgetName, student?.studentName ?: context.getString(R.string.all_no_data))
setRemoteAdapter(R.id.timetableWidgetList, Intent(context, TimetableWidgetService::class.java)
.apply { action = createWidgetKey(appWidgetId) })
.apply { putExtra(EXTRA_APPWIDGET_ID, appWidgetId) })
setOnClickPendingIntent(R.id.timetableWidgetNext, createNavIntent(context, appWidgetId, appWidgetId, BUTTON_NEXT))
setOnClickPendingIntent(R.id.timetableWidgetPrev, createNavIntent(context, -appWidgetId, appWidgetId, BUTTON_PREV))
createNavIntent(context, Int.MAX_VALUE - appWidgetId, appWidgetId, BUTTON_RESET).also {
createNavIntent(context, Int.MAX_VALUE - appWidgetId, appWidgetId, BUTTON_RESET).let {
setOnClickPendingIntent(R.id.timetableWidgetDate, it)
setOnClickPendingIntent(R.id.timetableWidgetDay, it)
setOnClickPendingIntent(R.id.timetableWidgetName, it)
}
setOnClickPendingIntent(R.id.timetableWidgetAccount, PendingIntent.getActivity(context, -Int.MAX_VALUE + appWidgetId,
Intent(context, TimetableWidgetConfigureActivity::class.java).apply {
addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK)
putExtra(EXTRA_APPWIDGET_ID, appWidgetId)
putExtra(EXTRA_FROM_PROVIDER, true)
}, FLAG_UPDATE_CURRENT))
setPendingIntentTemplate(R.id.timetableWidgetList,
PendingIntent.getActivity(context, 1, MainActivity.getStartIntent(context).apply {
putExtra(EXTRA_START_MENU_INDEX, 3)
putExtra(EXTRA_START_MENU, MainView.MenuView.TIMETABLE)
}, FLAG_UPDATE_CURRENT))
}.also {
sharedPref.putLong(createWidgetKey(appWidgetId), date.toEpochDay(), true)
sharedPref.putLong(getDateWidgetKey(appWidgetId), date.toEpochDay(), true)
appWidgetManager.apply {
notifyAppWidgetViewDataChanged(appWidgetId, R.id.timetableWidgetList)
updateAppWidget(appWidgetId, it)
@ -120,4 +151,31 @@ class TimetableWidgetProvider : BroadcastReceiver() {
putExtra(EXTRA_TOGGLED_WIDGET_ID, appWidgetId)
}, FLAG_UPDATE_CURRENT)
}
private fun getStudent(studentId: Long, appWidgetId: Int): Student? {
return try {
studentRepository.isStudentSaved()
.filter { true }
.flatMap { studentRepository.getSavedStudents(false).toMaybe() }
.flatMap { students ->
students.singleOrNull { student -> student.id == studentId }
.let { student ->
when {
student != null -> Maybe.just(student)
studentId != 0L -> {
studentRepository.getCurrentStudent(false)
.toMaybe()
.doOnSuccess { sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id) }
}
else -> null
}
}
}
.subscribeOn(schedulers.backgroundThread)
.blockingGet()
} catch (e: Exception) {
Timber.e(e, "An error has occurred in timetable widget provider")
null
}
}
}

View File

@ -2,10 +2,11 @@ package io.github.wulkanowy.utils
import android.os.Bundle
import com.google.firebase.analytics.FirebaseAnalytics
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class FirebaseAnalyticsHelper(private val analytics: FirebaseAnalytics) {
class FirebaseAnalyticsHelper @Inject constructor(private val analytics: FirebaseAnalytics) {
fun logEvent(name: String, vararg params: Pair<String, Any?>) {
Bundle().apply {

View File

@ -1,7 +1,16 @@
package io.github.wulkanowy.utils
import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.crashlytics.android.Crashlytics
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
class DebugLogTree : Timber.DebugTree() {
@ -20,3 +29,88 @@ class CrashlyticsTree : Timber.Tree() {
else Crashlytics.logException(t)
}
}
class ActivityLifecycleLogger : Application.ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity?) {
activity?.let { Timber.d("${it::class.java.simpleName} PAUSED") }
}
override fun onActivityResumed(activity: Activity?) {
activity?.let { Timber.d("${it::class.java.simpleName} RESUMED") }
}
override fun onActivityStarted(activity: Activity?) {
activity?.let { Timber.d("${it::class.java.simpleName} STARTED") }
}
override fun onActivityDestroyed(activity: Activity?) {
activity?.let { Timber.d("${it::class.java.simpleName} DESTROYED") }
}
override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {
activity?.let { Timber.d("${it::class.java.simpleName} SAVED INSTANCE STATE") }
}
override fun onActivityStopped(activity: Activity?) {
activity?.let { Timber.d("${it::class.java.simpleName} STOPPED") }
}
override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {
activity?.let { Timber.d("${it::class.java.simpleName} CREATED ${checkSavedState(savedInstanceState)}") }
}
}
@Singleton
class FragmentLifecycleLogger @Inject constructor() : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewCreated(fm: FragmentManager, f: Fragment, v: View, savedInstanceState: Bundle?) {
Timber.d("${f::class.java.simpleName} VIEW CREATED ${checkSavedState(savedInstanceState)}")
}
override fun onFragmentStopped(fm: FragmentManager, f: Fragment) {
Timber.d("${f::class.java.simpleName} STOPPED")
}
override fun onFragmentCreated(fm: FragmentManager, f: Fragment, savedInstanceState: Bundle?) {
Timber.d("${f::class.java.simpleName} CREATED ${checkSavedState(savedInstanceState)}")
}
override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
Timber.d("${f::class.java.simpleName} RESUMED")
}
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
Timber.d("${f::class.java.simpleName} ATTACHED")
}
override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
Timber.d("${f::class.java.simpleName} DESTROYED")
}
override fun onFragmentSaveInstanceState(fm: FragmentManager, f: Fragment, outState: Bundle) {
Timber.d("${f::class.java.simpleName} SAVED INSTANCE STATE")
}
override fun onFragmentStarted(fm: FragmentManager, f: Fragment) {
Timber.d("${f::class.java.simpleName} STARTED")
}
override fun onFragmentViewDestroyed(fm: FragmentManager, f: Fragment) {
Timber.d("${f::class.java.simpleName} VIEW DESTROYED")
}
override fun onFragmentActivityCreated(fm: FragmentManager, f: Fragment, savedInstanceState: Bundle?) {
Timber.d("${f::class.java.simpleName} ACTIVITY CREATED ${checkSavedState(savedInstanceState)}")
}
override fun onFragmentPaused(fm: FragmentManager, f: Fragment) {
Timber.d("${f::class.java.simpleName} PAUSED")
}
override fun onFragmentDetached(fm: FragmentManager, f: Fragment) {
Timber.d("${f::class.java.simpleName} DETACHED")
}
}
private fun checkSavedState(savedInstanceState: Bundle?) = if (savedInstanceState == null) "(STATE IS NULL)" else ""

View File

@ -99,6 +99,9 @@ inline val LocalDate.previousOrSameSchoolDay: LocalDate
inline val LocalDate.weekDayName: String
get() = this.format(ofPattern("EEEE", Locale.getDefault()))
inline val LocalDate.shortcutWeekDayName: String
get() = this.format(ofPattern("EEE", Locale.getDefault()))
inline val LocalDate.monday: LocalDate
get() = this.with(MONDAY)

View File

@ -1,11 +1,13 @@
Wersja 0.7.5
Wersja 0.8.0
Uwaga! Jeżeli w aplikacji przestały wyświetlać się oceny, prosimy o wylogowanie i zalogowanie się ponownie!
Uwaga! Po tej aktualizacji wymagane jest ponowne przypięcie widżetu planu lekcji!
Naprawiliśmy:
- problem z brakiem aktywnego semestru
- logowanie w niestandardowych dziennikach na vulcan.net.pl
- oznaczanie lekcji w planie jako odwołanej, jeśli brak opisu tej zmiany
- ładowanie wiadomości, jeśli byli zalogowani jednocześnie uczeń i rodzic
Dodaliśmy:
- możliwość przesyłania dalej i usuwania wiadomości
- możliwość zmiany ucznia w widżecie
- opcję liczenia średniej ocen z całego roku
- tryb AMOLED
- logowanie wielu uczniów jednocześnie
- widżet szczęśliwego numerka
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases/tag/0.7.5
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases/tag/0.8.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 B

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#ffffffff" />
<corners android:radius="5dp" />
</shape>

View File

@ -2,7 +2,7 @@
<solid android:color="@null" />
<stroke
android:width="1dip"
android:color="#61000000" />
android:color="@color/spinner_stroke" />
<corners android:radius="4dip" />
<padding
android:bottom="0dip"

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,8V4l8,8 -8,8v-4H4V8z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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="280dp"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/widgetConfigureTitle"
android:layout_width="match_parent"
android:layout_height="64dp"
android:paddingStart="24dp"
android:paddingLeft="24dp"
android:paddingEnd="24dp"
android:paddingRight="24dp"
android:text="@string/account_title"
android:textColor="?android:textColorPrimary"
android:textSize="20sp"
android:textStyle="bold"
app:firstBaselineToTopHeight="40dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/widgetConfigureRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/widgetConfigureTitle"
android:layout_marginBottom="16dp"
android:overScrollMode="never"
tools:itemCount="3"
tools:listitem="@layout/item_account" />
</RelativeLayout>

View File

@ -30,7 +30,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="48dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:gravity="center_horizontal"
@ -39,7 +39,6 @@
app:fontFamily="sans-serif-light"
app:layout_constraintBottom_toTopOf="@+id/loginFormNameLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
@ -59,7 +58,6 @@
app:errorEnabled="true"
app:layout_constraintBottom_toTopOf="@+id/loginFormPassLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormHeader">
@ -87,7 +85,6 @@
app:errorEnabled="true"
app:layout_constraintBottom_toTopOf="@+id/loginFormHostLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormNameLayout"
app:passwordToggleEnabled="true">
@ -117,7 +114,6 @@
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/loginFormSignIn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormPassLayout">
@ -134,7 +130,7 @@
android:layout_marginStart="7dp"
android:layout_marginLeft="7dp"
android:layout_marginBottom="48dp"
android:background="?android:colorBackground"
android:background="?android:windowBackground"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:text="@string/login_host_hint"
@ -145,15 +141,6 @@
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/loginFormHostLayout" />
<TextView
android:id="@+id/loginFormPrivacyPolicyLink"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/login_privacy_policy"
app:layout_constraintStart_toStartOf="@+id/loginFormHostLayout"
app:layout_constraintTop_toBottomOf="@+id/loginFormHostLayout" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginFormSignIn"
style="@style/Widget.MaterialComponents.Button"
@ -163,7 +150,7 @@
android:layout_marginTop="48dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="48dp"
android:layout_marginBottom="16dp"
android:text="@string/login_sign_in"
android:textAppearance="?android:textAppearanceSmall"
android:textColor="@android:color/white"
@ -171,14 +158,12 @@
app:backgroundTint="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormHostLayout"
app:layout_constraintVertical_bias="1.0" />
app:layout_constraintTop_toBottomOf="@+id/loginFormHostLayout" />
<TextView
android:id="@+id/loginFormVersion"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="24dp"
android:layout_marginLeft="24dp"
android:layout_marginEnd="16dp"
@ -186,11 +171,31 @@
android:maxLines="2"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@id/loginFormSignIn"
app:layout_constraintEnd_toStartOf="@+id/loginFormSignIn"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/loginFormSignIn"
tools:text="Version 1.0.0" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginFormPrivacyLink"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:text="@string/login_privacy_policy"
android:textAppearance="?android:textAppearance"
android:textColor="@color/colorPrimary"
android:visibility="invisible"
app:backgroundTint="?android:windowBackground"
app:fontFamily="sans-serif-medium"
app:layout_constraintBottom_toBottomOf="@id/loginFormSignIn"
app:layout_constraintEnd_toStartOf="@+id/loginFormSignIn"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@id/loginFormHostLayout"
app:layout_constraintTop_toTopOf="@+id/loginFormSignIn"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -1,4 +1,5 @@
<FrameLayout 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="match_parent"
@ -12,10 +13,60 @@
android:indeterminate="true"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/loginStudentSelectRecycler"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/loginStudentSelectContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:itemCount="5"
tools:listitem="@layout/item_login_student_select" />
android:layout_height="match_parent">
<TextView
android:id="@+id/loginStudentSelectHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:layout_marginBottom="32dp"
android:gravity="center_horizontal"
android:text="@string/login_select_student"
android:textSize="16sp"
app:fontFamily="sans-serif-light"
app:layout_constraintBottom_toTopOf="@id/loginStudentSelectRecycler"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/loginStudentSelectRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="144dp"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@id/loginStudentSelectSignIn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="432dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginStudentSelectHeader"
tools:itemCount="6"
tools:listitem="@layout/item_login_student_select" />
<com.google.android.material.button.MaterialButton
android:id="@+id/loginStudentSelectSignIn"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="32dp"
android:text="@string/login_sign_in"
android:textAppearance="?android:textAppearanceSmall"
android:textColor="@android:color/white"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginStudentSelectRecycler" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -29,7 +29,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginLeft="32dp"
android:layout_marginTop="48dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:gravity="center_horizontal"
@ -79,11 +79,10 @@
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="48dp"
android:layout_marginEnd="24dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="48dp"
android:layout_marginBottom="16dp"
android:text="@string/login_sign_in"
android:textAppearance="?android:textAppearanceSmall"
android:textColor="@android:color/white"
@ -91,8 +90,7 @@
app:backgroundTint="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginSymbolNameLayout"
app:layout_constraintVertical_bias="1" />
app:layout_constraintTop_toBottomOf="@+id/loginSymbolNameLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>

View File

@ -10,6 +10,7 @@
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/messagePreviewContentContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"

View File

@ -33,7 +33,8 @@
android:ellipsize="end"
android:maxLines="1"
android:textSize="16sp"
tools:text="@tools:sample/lorem/random" />
tools:text="@tools:sample/lorem/random"
android:textColor="?android:textColorSecondary"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/accountItemSchool"
@ -47,6 +48,7 @@
android:layout_toRightOf="@id/accountItemImage"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
tools:text="@tools:sample/lorem/random" />
</RelativeLayout>

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