Compare commits

...

63 Commits

Author SHA1 Message Date
f79b7eaf83 [3.9.11-dev] 2019-11-24 21:47:05 +01:00
ae13bf946f [Home] Remove useless dummy cards. 2019-11-24 21:21:37 +01:00
f116c4f1f4 [Home] Implement basic timetable card. 2019-11-24 21:15:01 +01:00
867c8920a8 [APIv2/Messages] Add downloading attachments. 2019-11-24 20:55:04 +01:00
6e6dd34872 [UI] Add new Home fragment. Add Lucky number card and number selection dialog. 2019-11-24 19:41:17 +01:00
0759468fa7 [Sync] Add syncing all to manual sync dialog. 2019-11-24 16:47:10 +01:00
1b1fb09211 [APIv2/Vulcan] Fix problems with week start in timetable. 2019-11-24 16:31:51 +01:00
de414c912c [Sync] Fix error when user selects no features. 2019-11-24 13:20:42 +01:00
d274a2fed1 [Timetable] Change date receiver argument to timetableDate. 2019-11-24 13:20:22 +01:00
285b7e9b9e [Timetable] Make going to the specified date on the notification click. 2019-11-24 12:57:00 +01:00
875efcff7e [APIv2/Timetable] Fix ID in lessons. 2019-11-24 12:11:39 +01:00
07ae37167d [Notifications/Timetable] Make notifications for timetable changes 2019-11-24 11:09:45 +01:00
f689f4d427 [APIv2/Timetable] Fix lesson changes metadata. 2019-11-24 10:36:20 +01:00
19bc2b8b37 [3.9.10-dev] New UI + stability fixes 2019-11-23 23:26:19 +01:00
673116e27e [Settings/About] Add a new developer to about! 2019-11-23 23:09:03 +01:00
59fcb0a050 [APIv2/Timetable] Add lesson change metadata only when the lesson is today or in the future 2019-11-23 22:40:20 +01:00
cd76f99bbf [APIv2/Timetable] Add showing unread lesson changes 2019-11-23 22:26:21 +01:00
6a4994b9c2 [APIv2/Timetable] Make swipe refresh download timetable for the selected week 2019-11-23 21:57:30 +01:00
63960c5e05 [APIv2/Timetable] Add selecting date, marking as read and fix stepForward in Date 2019-11-23 21:27:52 +01:00
540afb6a28 [Home] Start making new home timetable card in Kotlin 2019-11-23 19:41:55 +01:00
ae10b8abbd [APIv2/Idziennik] Add new timetable getting and fix week start 2019-11-23 19:40:32 +01:00
db2ebab879 [APIv2/Vulcan] Add missing Lublin endpoints 2019-11-23 19:39:13 +01:00
6ec3d062df [UI] Update header image. Fix fragment bundle passing. 2019-11-23 19:37:00 +01:00
86b6060a09 [UI] Migrate to outlined icons. 2019-11-23 18:32:18 +01:00
83d123e341 [UI] New notifications view. 2019-11-22 22:41:40 +01:00
34061695f9 [UI] Update colored placeholder icons. 2019-11-22 19:23:49 +01:00
de68476442 [UI/Event] Add Sync text to manual event dialog. 2019-11-22 18:42:45 +01:00
678a81a44b [APIv2/Vulcan] Improve Vulcan login when migrating from APIv1. 2019-11-22 18:42:11 +01:00
cfb3096d53 [Sync] Add better task cancelling and better frozen task detection. 2019-11-22 18:41:15 +01:00
9b7aca745a [3.9.9-dev] 2019-11-21 19:18:04 +01:00
82852389fa [UI] Update indentation, again. Fix manual event color selecting. 2019-11-21 18:43:01 +01:00
ce06084e6f [Timetable] Fix lessons removing, again. 2019-11-21 18:24:02 +01:00
3ca051983f [APIv2/Timetable] Add searching for the next lesson by team id 2019-11-20 22:43:48 +01:00
cd379e4175 [APIv2/Librus] Add getting grade comments 2019-11-20 21:16:18 +01:00
62fdfa2b6f [UI] Update manual event dialog. Fix timetable errors. 2019-11-20 21:13:43 +01:00
9866017f7e [APIv2/Vulcan] Fix timetable teams issue. Fix missing login data error. 2019-11-20 19:51:50 +01:00
67f98b08c6 [Update] Update update code to allow update from direct download. 2019-11-20 17:11:08 +01:00
fdb5f7ec02 [APIv2/Mobidziennik] Implement getting message details. 2019-11-18 22:57:23 +01:00
04c3c7ca6e [APIv2] Handle Librus Portal maintenance. 2019-11-18 18:55:52 +01:00
f424315d97 [3.9.8-dev] 2019-11-17 23:17:37 +01:00
c907a8df37 [APIv2] Librus: better error handling. Timetable: fix widget crashing with NPE. 2019-11-17 23:16:13 +01:00
37ea65e3fc [Timetable] Add SwipeToRefresh. Select start&end hours based on lesson ranges. 2019-11-16 21:16:18 +01:00
a3e5f824c8 [UI] Fix messages & homework refresh layout sensitivity. 2019-11-16 18:46:01 +01:00
e0c850a455 [Gradle] Update Tachyon to support creating DayView programmatically. 2019-11-16 17:07:07 +01:00
1c6815f708 [3.9.7-dev] 2019-11-15 22:00:18 +01:00
9a20511935 [UI] Set pull-to-refresh colors. 2019-11-15 21:16:58 +01:00
965f5e73d9 [Bugs] Update gradle. Fix crashes in the timetable widget. 2019-11-15 19:55:46 +01:00
13b58a1d56 [3.9.6-dev] Widgets. Timetables. RIP APIv1. 2019-11-15 00:02:00 +01:00
0a3261b8b3 [APIv2/Mobidziennik] Fix incorrect profile ID in teams & timetable. 2019-11-14 23:46:53 +01:00
dbdfc7fdd8 [Widget] Add new Timetable widget with APIv2. 2019-11-14 23:33:13 +01:00
56062f5bfa [Timetable] Fix day fragment crashing on restoring saved instance 2019-11-14 22:13:32 +01:00
0cbba2eb45 [Models] Make Time comparable. Implement faster Date.stepForward 2019-11-14 19:31:50 +01:00
aa84356dd6 [APIv2/Vulcan] Add getting timetable with lesson changes 2019-11-14 00:41:34 +01:00
efaad0a4dd [APIv1] Remove remaining APIv1 components. [*] 2019-11-13 22:37:30 +01:00
71015c0137 [APIv1] Remove most APIv1 components. 2019-11-13 22:26:12 +01:00
85b5667a7e [Sync] New manual sync dialog. Remove most APIv1 dependencies. 2019-11-13 21:57:47 +01:00
dfdc6817a1 [APIv2/Vulcan] Fix getting sent messages with unknown recipient 2019-11-13 21:01:09 +01:00
058345b9c9 [Error] Add user friendly error strings. Add error snackbar to activity & login. 2019-11-13 19:44:08 +01:00
831b7876b4 [APIv2] Fix duplicated error code 2019-11-13 18:23:21 +01:00
729cf6f08e [Settings] New Profile removal dialog. 2019-11-13 17:19:25 +01:00
860a16b32d [3.9.5-dev] Messages & manual events 2019-11-13 17:19:25 +01:00
9f871c077b [APIv2/Librus] Fix getting messages with unknown recipient 2019-11-13 00:00:28 +01:00
a8f89abf7d [APIv2/Vulcan] Add changing message status 2019-11-12 23:59:48 +01:00
179 changed files with 5556 additions and 13930 deletions

2
.gitignore vendored
View File

@ -81,3 +81,5 @@ lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
app/schemas/

View File

@ -169,7 +169,7 @@ dependencies {
implementation 'com.github.kuba2k2:RecyclerTabLayout:700f980584'
implementation 'com.linkedin.android.tachyon:tachyon:1.0.2'
implementation 'com.github.kuba2k2:Tachyon:551943a6b5'
}
repositories {
mavenCentral()

View File

@ -105,6 +105,11 @@
android:excludeFromRecents="true"
android:noHistory="true"
android:theme="@style/AppTheme.NoDisplay" />
<activity android:name=".widgets.timetable.LessonDialogActivity"
android:configChanges="orientation|keyboardHidden"
android:excludeFromRecents="true"
android:noHistory="true"
android:theme="@style/AppTheme.NoDisplay" />
<activity
android:name=".ui.modules.settings.SettingsLicenseActivity"
android:configChanges="orientation|keyboardHidden"

View File

@ -68,11 +68,6 @@ import me.leolin.shortcutbadger.ShortcutBadger;
import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient;
import okhttp3.TlsVersion;
import pl.szczodrzynski.edziennik.data.api.Edziennik;
import pl.szczodrzynski.edziennik.data.api.Iuczniowie;
import pl.szczodrzynski.edziennik.data.api.Librus;
import pl.szczodrzynski.edziennik.data.api.Mobidziennik;
import pl.szczodrzynski.edziennik.data.api.Vulcan;
import pl.szczodrzynski.edziennik.data.db.AppDb;
import pl.szczodrzynski.edziennik.data.db.modules.debuglog.DebugLog;
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore;
@ -141,12 +136,6 @@ public class App extends androidx.multidex.MultiDexApplication implements Config
public PersistentCookieJar cookieJar;
public OkHttpClient http;
public OkHttpClient httpLazy;
//public Jakdojade apiJakdojade;
public Edziennik apiEdziennik;
public Mobidziennik apiMobidziennik;
public Iuczniowie apiIuczniowie;
public Librus apiLibrus;
public Vulcan apiVulcan;
public SharedPreferences appSharedPrefs; // sharedPreferences for APPCONFIG + JOBS STORE
public AppConfig appConfig; // APPCONFIG: common for all profiles
@ -204,12 +193,6 @@ public class App extends androidx.multidex.MultiDexApplication implements Config
db = AppDb.getDatabase(this);
gson = new Gson();
networkUtils = new NetworkUtils(this);
apiEdziennik = new Edziennik(this);
//apiJakdojade = new Jakdojade(this);
apiMobidziennik = new Mobidziennik(this);
apiIuczniowie = new Iuczniowie(this);
apiLibrus = new Librus(this);
apiVulcan = new Vulcan(this);
Iconics.init(getApplicationContext());
Iconics.registerFont(SzkolnyFont.INSTANCE);

View File

@ -4,7 +4,9 @@ import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.text.*
@ -13,9 +15,11 @@ import android.text.style.StrikethroughSpan
import android.text.style.StyleSpan
import android.util.LongSparseArray
import android.util.SparseArray
import android.util.TypedValue
import android.view.View
import android.widget.CompoundButton
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.annotation.*
import androidx.core.app.ActivityCompat
import androidx.core.util.forEach
import androidx.lifecycle.LifecycleOwner
@ -25,9 +29,13 @@ import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import im.wangchao.mhttp.Response
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.data.db.modules.teams.Team
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.navlib.R
import pl.szczodrzynski.navlib.getColorFromRes
import java.text.SimpleDateFormat
@ -95,6 +103,15 @@ fun String.swapFirstLastName(): String {
}
}
fun String.getFirstLastName(): Pair<String, String>? {
return this.split(" ").let {
if (it.size >= 2) Pair(it[0], it[1])
else null
}
}
fun String.getLastFirstName() = this.getFirstLastName()
fun changeStringCase(s: String): String {
val delimiters = " '-/"
val sb = StringBuilder()
@ -153,8 +170,9 @@ fun colorFromName(context: Context, name: String?): Int {
return context.getColorFromRes(color)
}
fun MutableList<out Profile>.filterOutArchived() {
fun MutableList<Profile>.filterOutArchived(): MutableList<Profile> {
this.removeAll { it.archived }
return this
}
fun Activity.isStoragePermissionGranted(): Boolean {
@ -404,15 +422,15 @@ fun TextView.setText(@StringRes resid: Int, vararg formatArgs: Any) {
text = context.getString(resid, *formatArgs)
}
fun JsonObject(vararg properties: Pair<String, Any>): JsonObject {
fun JsonObject(vararg properties: Pair<String, Any?>): JsonObject {
return JsonObject().apply {
for (property in properties) {
when (property.second) {
is JsonElement -> add(property.first, property.second as JsonElement)
is String -> addProperty(property.first, property.second as String)
is Char -> addProperty(property.first, property.second as Char)
is Number -> addProperty(property.first, property.second as Number)
is Boolean -> addProperty(property.first, property.second as Boolean)
is JsonElement -> add(property.first, property.second as JsonElement?)
is String -> addProperty(property.first, property.second as String?)
is Char -> addProperty(property.first, property.second as Char?)
is Number -> addProperty(property.first, property.second as Number?)
is Boolean -> addProperty(property.first, property.second as Boolean?)
}
}
}
@ -428,6 +446,13 @@ inline fun <T : View> T.onClick(crossinline onClickListener: (v: T) -> Unit) {
}
}
@Suppress("UNCHECKED_CAST")
inline fun <T : CompoundButton> T.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) {
setOnCheckedChangeListener { buttonView, isChecked ->
onChangeListener(buttonView as T, isChecked)
}
}
fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) {
observe(lifecycleOwner, object : Observer<T> {
override fun onChanged(t: T?) {
@ -435,4 +460,76 @@ fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observ
removeObserver(this)
}
})
}
}
/**
* Convert a value in dp to pixels.
*/
val Int.dp: Int
get() = (this * Resources.getSystem().displayMetrics.density).toInt()
/**
* Convert a value in pixels to dp.
*/
val Int.px: Int
get() = (this / Resources.getSystem().displayMetrics.density).toInt()
@ColorInt
fun @receiver:AttrRes Int.resolveAttr(context: Context?): Int {
val typedValue = TypedValue()
context?.theme?.resolveAttribute(this, typedValue, true)
return typedValue.data
}
@ColorInt
fun @receiver:ColorRes Int.resolveColor(context: Context): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
context.resources.getColor(this, context.theme)
}
else {
context.resources.getColor(this)
}
}
fun @receiver:DrawableRes Int.resolveDrawable(context: Context): Drawable {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
context.resources.getDrawable(this, context.theme)
}
else {
context.resources.getDrawable(this)
}
}
fun View.findParentById(targetId: Int): View? {
if (id == targetId) {
return this
}
val viewParent = this.parent ?: return null
if (viewParent is View) {
return viewParent.findParentById(targetId)
}
return null
}
fun CoroutineScope.startCoroutineTimer(delayMillis: Long = 0, repeatMillis: Long = 0, action: () -> Unit) = launch {
delay(delayMillis)
if (repeatMillis > 0) {
while (true) {
action()
delay(repeatMillis)
}
} else {
action()
}
}
operator fun Time?.compareTo(other: Time?): Int {
if (this == null && other == null)
return 0
if (this == null)
return -1
if (other == null)
return 1
return this.compareTo(other)
}
operator fun StringBuilder.plusAssign(str: String?) {
this.append(str)
}

View File

@ -38,23 +38,25 @@ import pl.droidsonroids.gif.GifDrawable
import pl.szczodrzynski.edziennik.App.APP_URL
import pl.szczodrzynski.edziennik.api.v2.events.*
import pl.szczodrzynski.edziennik.api.v2.events.task.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface.*
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata.*
import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding
import pl.szczodrzynski.edziennik.network.ServerRequest
import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent
import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog
import pl.szczodrzynski.edziennik.ui.modules.agenda.AgendaFragment
import pl.szczodrzynski.edziennik.ui.modules.announcements.AnnouncementsFragment
import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceFragment
import pl.szczodrzynski.edziennik.ui.modules.base.DebugFragment
import pl.szczodrzynski.edziennik.ui.modules.behaviour.BehaviourFragment
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar
import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackFragment
import pl.szczodrzynski.edziennik.ui.modules.feedback.HelpFragment
import pl.szczodrzynski.edziennik.ui.modules.grades.GradesFragment
import pl.szczodrzynski.edziennik.ui.modules.grades.editor.GradesEditorFragment
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragmentV2
import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkFragment
import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity
import pl.szczodrzynski.edziennik.ui.modules.messages.MessageFragment
@ -120,9 +122,9 @@ class MainActivity : AppCompatActivity() {
val list: MutableList<NavTarget> = mutableListOf()
// home item
list += NavTarget(DRAWER_ITEM_HOME, R.string.menu_home_page, HomeFragment::class)
list += NavTarget(DRAWER_ITEM_HOME, R.string.menu_home_page, HomeFragmentV2::class)
.withTitle(R.string.app_name)
.withIcon(CommunityMaterial.Icon2.cmd_home)
.withIcon(CommunityMaterial.Icon2.cmd_home_outline)
.isInDrawer(true)
.isStatic(true)
.withPopToHome(false)
@ -133,50 +135,50 @@ class MainActivity : AppCompatActivity() {
.isInDrawer(true)
list += NavTarget(DRAWER_ITEM_AGENDA, R.string.menu_agenda, AgendaFragment::class)
.withIcon(CommunityMaterial.Icon.cmd_calendar)
.withIcon(CommunityMaterial.Icon.cmd_calendar_outline)
.withBadgeTypeId(TYPE_EVENT)
.isInDrawer(true)
list += NavTarget(DRAWER_ITEM_GRADES, R.string.menu_grades, GradesFragment::class)
.withIcon(CommunityMaterial.Icon2.cmd_numeric_5_box)
.withIcon(CommunityMaterial.Icon2.cmd_numeric_5_box_outline)
.withBadgeTypeId(TYPE_GRADE)
.isInDrawer(true)
list += NavTarget(DRAWER_ITEM_MESSAGES, R.string.menu_messages, MessagesFragment::class)
.withIcon(CommunityMaterial.Icon.cmd_email)
.withIcon(CommunityMaterial.Icon.cmd_email_outline)
.withBadgeTypeId(TYPE_MESSAGE)
.isInDrawer(true)
list += NavTarget(DRAWER_ITEM_HOMEWORK, R.string.menu_homework, HomeworkFragment::class)
.withIcon(SzkolnyFont.Icon.szf_file_document_edit)
.withIcon(SzkolnyFont.Icon.szf_notebook_outline)
.withBadgeTypeId(TYPE_HOMEWORK)
.isInDrawer(true)
list += NavTarget(DRAWER_ITEM_BEHAVIOUR, R.string.menu_notices, BehaviourFragment::class)
.withIcon(CommunityMaterial.Icon2.cmd_message_alert)
.withIcon(CommunityMaterial.Icon.cmd_emoticon_outline)
.withBadgeTypeId(TYPE_NOTICE)
.isInDrawer(true)
list += NavTarget(DRAWER_ITEM_ATTENDANCE, R.string.menu_attendance, AttendanceFragment::class)
.withIcon(CommunityMaterial.Icon.cmd_calendar_remove)
.withIcon(CommunityMaterial.Icon.cmd_calendar_remove_outline)
.withBadgeTypeId(TYPE_ATTENDANCE)
.isInDrawer(true)
list += NavTarget(DRAWER_ITEM_ANNOUNCEMENTS, R.string.menu_announcements, AnnouncementsFragment::class)
.withIcon(CommunityMaterial.Icon.cmd_bulletin_board)
.withIcon(CommunityMaterial.Icon.cmd_bullhorn_outline)
.withBadgeTypeId(TYPE_ANNOUNCEMENT)
.isInDrawer(true)
// static drawer items
list += NavTarget(DRAWER_ITEM_NOTIFICATIONS, R.string.menu_notifications, NotificationsFragment::class)
.withIcon(CommunityMaterial.Icon.cmd_bell_ring)
.withIcon(CommunityMaterial.Icon.cmd_bell_ring_outline)
.isInDrawer(true)
.isStatic(true)
.isBelowSeparator(true)
list += NavTarget(DRAWER_ITEM_SETTINGS, R.string.menu_settings, SettingsNewFragment::class)
.withIcon(CommunityMaterial.Icon2.cmd_settings)
.withIcon(CommunityMaterial.Icon2.cmd_settings_outline)
.isInDrawer(true)
.isStatic(true)
.isBelowSeparator(true)
@ -195,7 +197,7 @@ class MainActivity : AppCompatActivity() {
.isInProfileList(false)
list += NavTarget(DRAWER_PROFILE_SYNC_ALL, R.string.menu_sync_all, null)
.withIcon(CommunityMaterial.Icon2.cmd_sync)
.withIcon(CommunityMaterial.Icon.cmd_download_outline)
.isInProfileList(true)
@ -214,6 +216,7 @@ class MainActivity : AppCompatActivity() {
val navView: NavView by lazy { b.navView }
val drawer: NavDrawer by lazy { navView.drawer }
val bottomSheet: NavBottomSheet by lazy { navView.bottomSheet }
val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) }
val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout }
@ -246,6 +249,8 @@ class MainActivity : AppCompatActivity() {
setContentView(b.root)
errorSnackbar.setCoordinator(b.navView.coordinator, b.navView.bottomBar)
navLoading = true
b.navView.apply {
@ -371,6 +376,11 @@ class MainActivity : AppCompatActivity() {
b.swipeRefreshLayout.isEnabled = true
b.swipeRefreshLayout.setOnRefreshListener { this.syncCurrentFeature() }
b.swipeRefreshLayout.setColorSchemeResources(
R.color.md_blue_500,
R.color.md_amber_500,
R.color.md_green_500
)
isStoragePermissionGranted()
@ -424,7 +434,7 @@ class MainActivity : AppCompatActivity() {
navView.coordinator.postDelayed({
CafeBar.builder(this)
.content(R.string.rate_snackbar_text)
.icon(IconicsDrawable(this).icon(CommunityMaterial.Icon2.cmd_star).size(IconicsSize.dp(20)).color(IconicsColor.colorInt(Themes.getPrimaryTextColor(this))))
.icon(IconicsDrawable(this).icon(CommunityMaterial.Icon2.cmd_star_outline).size(IconicsSize.dp(20)).color(IconicsColor.colorInt(Themes.getPrimaryTextColor(this))))
.positiveText(R.string.rate_snackbar_positive)
.positiveColor(-0xb350b0)
.negativeText(R.string.rate_snackbar_negative)
@ -461,25 +471,25 @@ class MainActivity : AppCompatActivity() {
bottomSheet.appendItems(
BottomSheetPrimaryItem(false)
.withTitle(R.string.menu_sync)
.withIcon(CommunityMaterial.Icon2.cmd_sync)
.withIcon(CommunityMaterial.Icon.cmd_download_outline)
.withOnClickListener(View.OnClickListener {
bottomSheet.close()
app.apiEdziennik.guiSyncFeature(app, this, App.profileId, R.string.sync_dialog_title, R.string.sync_dialog_text, R.string.sync_done, fragmentToFeature(navTargetId))
SyncViewListDialog(this, navTargetId)
}),
BottomSheetSeparatorItem(false),
BottomSheetPrimaryItem(false)
.withTitle(R.string.menu_settings)
.withIcon(CommunityMaterial.Icon2.cmd_settings)
.withIcon(CommunityMaterial.Icon2.cmd_settings_outline)
.withOnClickListener(View.OnClickListener { loadTarget(DRAWER_ITEM_SETTINGS) }),
BottomSheetPrimaryItem(false)
.withTitle(R.string.menu_feedback)
.withIcon(CommunityMaterial.Icon2.cmd_help_circle)
.withIcon(CommunityMaterial.Icon2.cmd_help_circle_outline)
.withOnClickListener(View.OnClickListener { loadTarget(TARGET_FEEDBACK) })
)
if (App.devMode) {
bottomSheet += BottomSheetPrimaryItem(false)
.withTitle(R.string.menu_debug)
.withIcon(CommunityMaterial.Icon.cmd_android_debug_bridge)
.withIcon(CommunityMaterial.Icon.cmd_android_studio)
.withOnClickListener(View.OnClickListener { loadTarget(DRAWER_ITEM_DEBUG) })
}
@ -527,9 +537,14 @@ class MainActivity : AppCompatActivity() {
DRAWER_ITEM_MESSAGES -> MessagesFragment.pageSelection
else -> 0
}
val arguments = when (navTargetId) {
DRAWER_ITEM_TIMETABLE -> JsonObject("weekStart" to TimetableFragment.pageSelection?.weekStart?.stringY_m_d)
else -> null
}
EdziennikTask.syncProfile(
App.profileId,
listOf(navTargetId to fragmentParam)
listOf(navTargetId to fragmentParam),
arguments
).enqueue(this)
}
@Subscribe(threadMode = ThreadMode.MAIN)
@ -573,7 +588,12 @@ class MainActivity : AppCompatActivity() {
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onSyncErrorEvent(event: ApiTaskErrorEvent) {
navView.toolbar.apply {
subtitleFormat = R.string.toolbar_subtitle
subtitleFormatWithUnread = R.plurals.toolbar_subtitle_with_unread
subtitle = "Gotowe"
}
errorSnackbar.addError(event.error).show()
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onAppManagerDetectedEvent(event: AppManagerDetectedEvent) {
@ -605,22 +625,7 @@ class MainActivity : AppCompatActivity() {
.setCancelable(false)
.show()
}
private fun fragmentToFeature(currentFragment: Int): Int {
return when (currentFragment) {
DRAWER_ITEM_TIMETABLE -> FEATURE_TIMETABLE
DRAWER_ITEM_AGENDA -> FEATURE_AGENDA
DRAWER_ITEM_GRADES -> FEATURE_GRADES
DRAWER_ITEM_HOMEWORK -> FEATURE_HOMEWORK
DRAWER_ITEM_BEHAVIOUR -> FEATURE_NOTICES
DRAWER_ITEM_ATTENDANCE -> FEATURE_ATTENDANCE
DRAWER_ITEM_MESSAGES -> when (MessagesFragment.pageSelection) {
1 -> FEATURE_MESSAGES_OUTBOX
else -> FEATURE_MESSAGES_INBOX
}
DRAWER_ITEM_ANNOUNCEMENTS -> FEATURE_ANNOUNCEMENTS
else -> FEATURE_ALL
}
}
private fun fragmentToSyncName(currentFragment: Int): Int {
return when (currentFragment) {
DRAWER_ITEM_TIMETABLE -> R.string.sync_feature_timetable
@ -694,10 +699,13 @@ class MainActivity : AppCompatActivity() {
app.profile == null -> {
if (intentProfileId == -1)
intentProfileId = app.appSharedPrefs.getInt("current_profile_id", 1)
loadProfile(intentProfileId, intentTargetId)
loadProfile(intentProfileId, intentTargetId, extras)
}
intentProfileId != -1 -> {
loadProfile(intentProfileId, intentTargetId)
if (app.profile.id != intentProfileId)
loadProfile(intentProfileId, intentTargetId, extras)
else
loadTarget(intentTargetId, extras)
}
intentTargetId != -1 -> {
drawer.currentProfile = app.profile.id
@ -1043,7 +1051,7 @@ class MainActivity : AppCompatActivity() {
}
loadTarget(DRAWER_ITEM_SETTINGS, null)
} else if (item.itemId == 2) {
app.apiEdziennik.guiRemoveProfile(this@MainActivity, profileId, profile.name?.getText(this).toString())
ProfileRemoveDialog(this, profileId, profile.name?.getText(this)?.toString() ?: "?")
}
true
}

View File

@ -311,13 +311,14 @@ public class Notifier {
\____/| .__/ \__,_|\__,_|\__\___||___/
| |
|*/
public void notificationUpdatesShow(String updateVersion, String updateUrl, String updateFilename) {
public void notificationUpdatesShow(String updateVersion, String updateUrl, String updateFilename, boolean updateDirect) {
if (!app.appConfig.notifyAboutUpdates)
return;
Intent notificationIntent = new Intent(app.getContext(), BootReceiver.NotificationActionService.class)
.putExtra("update_version", updateVersion)
.putExtra("update_url", updateUrl)
.putExtra("update_filename", updateFilename);
.putExtra("update_filename", updateFilename)
.putExtra("update_direct", updateDirect);
PendingIntent pendingIntent = PendingIntent.getService(app.getContext(), 0,
notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT);

View File

@ -1,450 +0,0 @@
package pl.szczodrzynski.edziennik;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.util.SparseArray;
import android.view.View;
import android.widget.RemoteViews;
import com.mikepenz.iconics.IconicsColor;
import com.mikepenz.iconics.IconicsDrawable;
import com.mikepenz.iconics.IconicsSize;
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import pl.szczodrzynski.edziennik.api.v2.events.task.EdziennikTask;
import pl.szczodrzynski.edziennik.data.db.modules.events.EventFull;
import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange;
import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonFull;
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile;
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment;
import pl.szczodrzynski.edziennik.utils.models.Date;
import pl.szczodrzynski.edziennik.utils.models.ItemWidgetTimetableModel;
import pl.szczodrzynski.edziennik.utils.models.Time;
import pl.szczodrzynski.edziennik.utils.models.Week;
import pl.szczodrzynski.edziennik.widgets.WidgetConfig;
import pl.szczodrzynski.edziennik.widgets.timetable.LessonDetailsActivity;
import pl.szczodrzynski.edziennik.widgets.timetable.WidgetTimetableService;
import static pl.szczodrzynski.edziennik.ExtensionsKt.filterOutArchived;
import static pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_HOMEWORK;
import static pl.szczodrzynski.edziennik.utils.Utils.bs;
public class WidgetTimetable extends AppWidgetProvider {
public static final String ACTION_SYNC_DATA = "ACTION_SYNC_DATA";
private static final String TAG = "WidgetTimetable";
private static int modeInt = 0;
public WidgetTimetable() {
// Start the worker thread
//HandlerThread sWorkerThread = new HandlerThread("WidgetTimetable-worker");
//sWorkerThread.start();
//Handler sWorkerQueue = new Handler(sWorkerThread.getLooper());
}
public static SparseArray<List<ItemWidgetTimetableModel>> timetables = null;
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_SYNC_DATA.equals(intent.getAction())) {
EdziennikTask.Companion.sync().enqueue(context);
}
super.onReceive(context, intent);
}
public static PendingIntent getPendingSelfIntent(Context context, String action) {
Intent intent = new Intent(context, WidgetTimetable.class);
intent.setAction(action);
return getPendingSelfIntent(context, intent);
}
public static PendingIntent getPendingSelfIntent(Context context, Intent intent) {
return PendingIntent.getBroadcast(context, 0, intent, 0);
}
public static Bitmap drawableToBitmap (Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable)drawable).getBitmap();
}
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
ComponentName thisWidget = new ComponentName(context, WidgetTimetable.class);
timetables = new SparseArray<>();
//timetables.clear();
App app = (App)context.getApplicationContext();
int[] allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
// There may be multiple widgets active, so update all of them
for (int appWidgetId : allWidgetIds) {
//d(TAG, "thr "+Thread.currentThread().getName());
WidgetConfig widgetConfig = app.appConfig.widgetTimetableConfigs.get(appWidgetId);
if (widgetConfig == null) {
widgetConfig = new WidgetConfig(app.profileFirstId());
app.appConfig.widgetTimetableConfigs.put(appWidgetId, widgetConfig);
app.appConfig.savePending = true;
}
RemoteViews views;
if (widgetConfig.bigStyle) {
views = new RemoteViews(context.getPackageName(), widgetConfig.darkTheme ? R.layout.widget_timetable_dark_big : R.layout.widget_timetable_big);
}
else {
views = new RemoteViews(context.getPackageName(), widgetConfig.darkTheme ? R.layout.widget_timetable_dark : R.layout.widget_timetable);
}
PorterDuff.Mode mode = PorterDuff.Mode.DST_IN;
/*if (widgetConfig.darkTheme) {
switch (modeInt) {
case 0:
mode = PorterDuff.Mode.ADD;
d(TAG, "ADD");
break;
case 1:
mode = PorterDuff.Mode.DST_ATOP;
d(TAG, "DST_ATOP");
break;
case 2:
mode = PorterDuff.Mode.DST_IN;
d(TAG, "DST_IN");
break;
case 3:
mode = PorterDuff.Mode.DST_OUT;
d(TAG, "DST_OUT");
break;
case 4:
mode = PorterDuff.Mode.DST_OVER;
d(TAG, "DST_OVER");
break;
case 5:
mode = PorterDuff.Mode.LIGHTEN;
d(TAG, "LIGHTEN");
break;
case 6:
mode = PorterDuff.Mode.MULTIPLY;
d(TAG, "MULTIPLY");
break;
case 7:
mode = PorterDuff.Mode.OVERLAY;
d(TAG, "OVERLAY");
break;
case 8:
mode = PorterDuff.Mode.SCREEN;
d(TAG, "SCREEN");
break;
case 9:
mode = PorterDuff.Mode.SRC_ATOP;
d(TAG, "SRC_ATOP");
break;
case 10:
mode = PorterDuff.Mode.SRC_IN;
d(TAG, "SRC_IN");
break;
case 11:
mode = PorterDuff.Mode.SRC_OUT;
d(TAG, "SRC_OUT");
break;
case 12:
mode = PorterDuff.Mode.SRC_OVER;
d(TAG, "SRC_OVER");
break;
case 13:
mode = PorterDuff.Mode.XOR;
d(TAG, "XOR");
break;
default:
modeInt = 0;
mode = PorterDuff.Mode.ADD;
d(TAG, "ADD");
break;
}
}*/
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
// this code seems to crash the launcher on >= P
float transparency = widgetConfig.opacity; //0...1
long colorFilter = 0x01000000L * (long) (255f * transparency);
try {
final Method[] declaredMethods = Class.forName("android.widget.RemoteViews").getDeclaredMethods();
final int len = declaredMethods.length;
if (len > 0) {
for (int m = 0; m < len; m++) {
final Method method = declaredMethods[m];
if (method.getName().equals("setDrawableParameters")) {
method.setAccessible(true);
method.invoke(views, R.id.widgetTimetableListView, true, -1, (int) colorFilter, mode, -1);
method.invoke(views, R.id.widgetTimetableHeader, true, -1, (int) colorFilter, mode, -1);
break;
}
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
Intent refreshIntent = new Intent(context, WidgetTimetable.class);
refreshIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
refreshIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
PendingIntent pendingRefreshIntent = PendingIntent.getBroadcast(context,
0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT);
views.setOnClickPendingIntent(R.id.widgetTimetableRefresh, pendingRefreshIntent);
views.setOnClickPendingIntent(R.id.widgetTimetableSync, WidgetTimetable.getPendingSelfIntent(context, ACTION_SYNC_DATA));
views.setImageViewBitmap(R.id.widgetTimetableRefresh, new IconicsDrawable(context, CommunityMaterial.Icon2.cmd_refresh)
.color(IconicsColor.colorInt(Color.WHITE))
.size(IconicsSize.dp(widgetConfig.bigStyle ? 24 : 16)).toBitmap());
views.setImageViewBitmap(R.id.widgetTimetableSync, new IconicsDrawable(context, CommunityMaterial.Icon2.cmd_sync)
.color(IconicsColor.colorInt(Color.WHITE))
.size(IconicsSize.dp(widgetConfig.bigStyle ? 24 : 16)).toBitmap());
boolean unified = widgetConfig.profileId == -1;
List<Profile> profileList = new ArrayList<>();
if (unified) {
profileList = app.db.profileDao().getAllNow();
filterOutArchived(profileList);
}
else {
Profile profile = app.db.profileDao().getFullByIdNow(widgetConfig.profileId);
if (profile != null) {
profileList.add(profile);
}
}
//d(TAG, "Profiles: "+ Arrays.toString(profileList.toArray()));
if (profileList == null || profileList.size() == 0) {
views.setViewVisibility(R.id.widgetTimetableLoading, View.VISIBLE);
views.setTextViewText(R.id.widgetTimetableLoading, app.getString(R.string.widget_timetable_profile_doesnt_exist));
}
else {
views.setViewVisibility(R.id.widgetTimetableLoading, View.GONE);
//Register profile;
long bellSyncDiffMillis = 0;
if (app.appConfig.bellSyncDiff != null) {
bellSyncDiffMillis = app.appConfig.bellSyncDiff.hour * 60 * 60 * 1000 + app.appConfig.bellSyncDiff.minute * 60 * 1000 + app.appConfig.bellSyncDiff.second * 1000;
bellSyncDiffMillis *= app.appConfig.bellSyncMultiplier;
bellSyncDiffMillis *= -1;
}
List<ItemWidgetTimetableModel> lessonList = new ArrayList<>();
Time syncedNow = Time.fromMillis(Time.getNow().getInMillis() + bellSyncDiffMillis);
Date today = Date.getToday();
int openProfileId = -1;
Date displayingDate = null;
int displayingWeekDay = 0;
if (unified) {
views.setTextViewText(R.id.widgetTimetableSubtitle, app.getString(R.string.widget_timetable_title_unified));
}
else {
views.setTextViewText(R.id.widgetTimetableSubtitle, profileList.get(0).getName());
openProfileId = profileList.get(0).getId();
}
List<LessonFull> lessons = app.db.lessonDao().getAllWeekNow(unified ? -1 : openProfileId, today.clone().stepForward(0, 0, -today.getWeekDay()), today);
int scrollPos = 0;
for (Profile profile: profileList) {
Date profileDisplayingDate = HomeFragment.findDateWithLessons(profile.getId(), lessons, syncedNow, 1);
int profileDisplayingWeekDay = profileDisplayingDate.getWeekDay();
int dayDiff = Date.diffDays(profileDisplayingDate, Date.getToday());
//d(TAG, "For profile "+profile.name+" displayingDate is "+profileDisplayingDate.getStringY_m_d());
if (displayingDate == null || profileDisplayingDate.getValue() < displayingDate.getValue()) {
displayingDate = profileDisplayingDate;
displayingWeekDay = profileDisplayingWeekDay;
//d(TAG, "Setting as global dd");
if (dayDiff == 0) {
views.setTextViewText(R.id.widgetTimetableTitle, app.getString(R.string.day_today_format, Week.getFullDayName(displayingWeekDay)));
} else if (dayDiff == 1) {
views.setTextViewText(R.id.widgetTimetableTitle, app.getString(R.string.day_tomorrow_format, Week.getFullDayName(displayingWeekDay)));
} else {
views.setTextViewText(R.id.widgetTimetableTitle, Week.getFullDayName(displayingWeekDay) + " " + profileDisplayingDate.getStringDm());
}
}
}
for (Profile profile: profileList) {
int pos = 0;
List<EventFull> events = app.db.eventDao().getAllByDateNow(profile.getId(), displayingDate);
if (events == null)
events = new ArrayList<>();
if (unified) {
ItemWidgetTimetableModel separator = new ItemWidgetTimetableModel();
separator.profileId = profile.getId();
separator.bigStyle = widgetConfig.bigStyle;
separator.darkTheme = widgetConfig.darkTheme;
separator.separatorProfileName = profile.getName();
lessonList.add(separator);
}
for (LessonFull lesson : lessons) {
//d(TAG, "Profile "+profile.id+" Lesson profileId "+lesson.profileId+" weekDay "+lesson.weekDay+", "+lesson);
if (profile.getId() != lesson.profileId || displayingWeekDay != lesson.weekDay)
continue;
//d(TAG, "Not skipped");
ItemWidgetTimetableModel model = new ItemWidgetTimetableModel();
model.bigStyle = widgetConfig.bigStyle;
model.darkTheme = widgetConfig.darkTheme;
model.profileId = profile.getId();
model.lessonDate = displayingDate;
model.startTime = lesson.startTime;
model.endTime = lesson.endTime;
model.lessonPassed = (syncedNow.getValue() > lesson.endTime.getValue()) && displayingWeekDay == Week.getTodayWeekDay();
model.lessonCurrent = (Time.inRange(lesson.startTime, lesson.endTime, syncedNow)) && displayingWeekDay == Week.getTodayWeekDay();
if (model.lessonCurrent) {
scrollPos = pos;
} else if (model.lessonPassed) {
scrollPos = pos + 1;
}
pos++;
model.subjectName = bs(lesson.subjectLongName);
model.classroomName = lesson.classroomName;
model.bellSyncDiffMillis = bellSyncDiffMillis;
if (lesson.changeId != 0) {
if (lesson.changeType == LessonChange.TYPE_CHANGE) {
model.lessonChange = true;
if (lesson.changedClassroomName()) {
model.newClassroomName = lesson.changeClassroomName;
}
if (lesson.changedSubjectLongName()) {
model.newSubjectName = lesson.changeSubjectLongName;
}
}
if (lesson.changeType == LessonChange.TYPE_CANCELLED) {
model.lessonCancelled = true;
}
}
for (EventFull event : events) {
if (event.startTime == null)
continue;
if (event.eventDate.getValue() == displayingDate.getValue()
&& event.startTime.getValue() == lesson.startTime.getValue()) {
model.eventColors.add(event.type == TYPE_HOMEWORK ? ItemWidgetTimetableModel.EVENT_COLOR_HOMEWORK : event.getColor());
}
}
lessonList.add(model);
}
}
if (lessonList.size() == 0) {
views.setViewVisibility(R.id.widgetTimetableLoading, View.VISIBLE);
views.setRemoteAdapter(R.id.widgetTimetableListView, new Intent());
views.setTextViewText(R.id.widgetTimetableLoading, app.getString(R.string.widget_timetable_no_lessons));
appWidgetManager.updateAppWidget(appWidgetId, views);
}
else {
views.setViewVisibility(R.id.widgetTimetableLoading, View.GONE);
timetables.put(appWidgetId, lessonList);
//WidgetTimetableListProvider.widgetsLessons.put(appWidgetId, lessons);
//views.setRemoteAdapter(R.id.widgetTimetableListView, new Intent());
Intent listIntent = new Intent(context, WidgetTimetableService.class);
listIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
listIntent.setData(Uri.parse(listIntent.toUri(Intent.URI_INTENT_SCHEME)));
views.setRemoteAdapter(R.id.widgetTimetableListView, listIntent);
// template to handle the click listener for each item
Intent intentTemplate = new Intent(context, LessonDetailsActivity.class);
// Old activities shouldn't be in the history stack
intentTemplate.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntentTimetable = PendingIntent.getActivity(context,
0,
intentTemplate,
0);
views.setPendingIntentTemplate(R.id.widgetTimetableListView, pendingIntentTimetable);
Intent openIntent = new Intent(context, MainActivity.class);
openIntent.setAction("android.intent.action.MAIN");
if (!unified) {
openIntent.putExtra("profileId", openProfileId);
openIntent.putExtra("timetableDate", displayingDate.getValue());
}
openIntent.putExtra("fragmentId", MainActivity.DRAWER_ITEM_TIMETABLE);
PendingIntent pendingOpenIntent = PendingIntent.getActivity(context,
appWidgetId, openIntent, PendingIntent.FLAG_UPDATE_CURRENT);
views.setOnClickPendingIntent(R.id.widgetTimetableHeader, pendingOpenIntent);
if (!unified)
views.setScrollPosition(R.id.widgetTimetableListView, scrollPos);
}
}
appWidgetManager.updateAppWidget(appWidgetId, views);
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widgetTimetableListView);
}
//modeInt++;
}
@Override
public void onEnabled(Context context) {
// Enter relevant functionality for when the first widget is created
}
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
App app = (App) context.getApplicationContext();
for (int appWidgetId: appWidgetIds) {
app.appConfig.widgetTimetableConfigs.remove(appWidgetId);
}
app.saveConfig("widgetTimetableConfigs");
}
}

View File

@ -0,0 +1,371 @@
package pl.szczodrzynski.edziennik
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.util.SparseArray
import android.view.View
import android.widget.RemoteViews
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import pl.szczodrzynski.edziennik.api.v2.events.task.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_HOMEWORK
import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.ItemWidgetTimetableModel
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
import pl.szczodrzynski.edziennik.widgets.WidgetConfig
import pl.szczodrzynski.edziennik.widgets.timetable.LessonDialogActivity
import pl.szczodrzynski.edziennik.widgets.timetable.WidgetTimetableService
import java.lang.reflect.InvocationTargetException
class WidgetTimetable : AppWidgetProvider() {
override fun onReceive(context: Context, intent: Intent) {
if (ACTION_SYNC_DATA == intent.action) {
EdziennikTask.sync().enqueue(context)
}
super.onReceive(context, intent)
}
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
val thisWidget = ComponentName(context, WidgetTimetable::class.java)
timetables = SparseArray()
//timetables.clear();
val app = context.applicationContext as App
var bellSyncDiffMillis: Long = 0
if (app.appConfig.bellSyncDiff != null) {
bellSyncDiffMillis = (app.appConfig.bellSyncDiff.hour * 60 * 60 * 1000 + app.appConfig.bellSyncDiff.minute * 60 * 1000 + app.appConfig.bellSyncDiff.second * 1000).toLong()
bellSyncDiffMillis *= app.appConfig.bellSyncMultiplier.toLong()
bellSyncDiffMillis *= -1
}
val allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
allWidgetIds?.forEach { appWidgetId ->
var widgetConfig = app.appConfig.widgetTimetableConfigs[appWidgetId]
if (widgetConfig == null) {
widgetConfig = WidgetConfig(app.profileFirstId())
app.appConfig.widgetTimetableConfigs[appWidgetId] = widgetConfig
app.appConfig.savePending = true
}
val views = if (widgetConfig.bigStyle) {
RemoteViews(context.packageName, if (widgetConfig.darkTheme) R.layout.widget_timetable_dark_big else R.layout.widget_timetable_big)
} else {
RemoteViews(context.packageName, if (widgetConfig.darkTheme) R.layout.widget_timetable_dark else R.layout.widget_timetable)
}
val refreshIntent = Intent(app, WidgetTimetable::class.java)
refreshIntent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
refreshIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
val pendingRefreshIntent = PendingIntent.getBroadcast(context,
0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.widgetTimetableRefresh, pendingRefreshIntent)
views.setOnClickPendingIntent(R.id.widgetTimetableSync, getPendingSelfIntent(context, ACTION_SYNC_DATA))
views.setImageViewBitmap(R.id.widgetTimetableRefresh, IconicsDrawable(context, CommunityMaterial.Icon2.cmd_refresh)
.colorInt(Color.WHITE)
.sizeDp(if (widgetConfig.bigStyle) 24 else 16).toBitmap())
views.setImageViewBitmap(R.id.widgetTimetableSync, IconicsDrawable(context, CommunityMaterial.Icon.cmd_download_outline)
.colorInt(Color.WHITE)
.sizeDp(if (widgetConfig.bigStyle) 24 else 16).toBitmap())
prepareAppWidget(app, appWidgetId, views, widgetConfig, bellSyncDiffMillis)
appWidgetManager.updateAppWidget(appWidgetId, views)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widgetTimetableListView)
}
}
private fun prepareAppWidget(
app: App,
appWidgetId: Int,
views: RemoteViews,
widgetConfig: WidgetConfig,
bellSyncDiffMillis: Long
) {
// get the current bell-synced time
val now = Time.fromMillis(Time.getNow().inMillis + bellSyncDiffMillis)
// set the widget transparency
val mode = PorterDuff.Mode.DST_IN
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
// this code seems to crash the launcher on >= P
val transparency = widgetConfig.opacity //0...1
val colorFilter = 0x01000000L * (255f * transparency).toLong()
try {
val declaredMethods = Class.forName("android.widget.RemoteViews").declaredMethods
val len = declaredMethods.size
if (len > 0) {
for (m in 0 until len) {
val method = declaredMethods[m]
if (method.name == "setDrawableParameters") {
method.isAccessible = true
method.invoke(views, R.id.widgetTimetableListView, true, -1, colorFilter.toInt(), mode, -1)
method.invoke(views, R.id.widgetTimetableHeader, true, -1, colorFilter.toInt(), mode, -1)
break
}
}
}
} catch (e: ClassNotFoundException) {
e.printStackTrace()
} catch (e: InvocationTargetException) {
e.printStackTrace()
} catch (e: IllegalAccessException) {
e.printStackTrace()
} catch (e: IllegalArgumentException) {
e.printStackTrace()
}
}
val unified = widgetConfig.profileId == -1
// get all profiles or one profile with the specified id
val profileList = if (unified)
app.db.profileDao().allNow.filterOutArchived()
else
listOfNotNull(app.db.profileDao().getByIdNow(widgetConfig.profileId))
// no profile was found
if (profileList.isEmpty()) {
views.setViewVisibility(R.id.widgetTimetableLoading, View.VISIBLE)
views.setTextViewText(R.id.widgetTimetableLoading, app.getString(R.string.widget_timetable_profile_doesnt_exist))
return
}
views.setViewVisibility(R.id.widgetTimetableLoading, View.GONE)
// set lesson search bounds
val today = Date.getToday()
val searchEnd = today.clone().stepForward(0, 0, 7)
var scrollPos = 0
var profileId: Int? = null
var displayingDate: Date? = null
val models = mutableListOf<ItemWidgetTimetableModel>()
// get all lessons within the search bounds
val lessonList = app.db.timetableDao().getBetweenDatesNow(today, searchEnd)
for (profile in profileList) {
// add a profile separator with its name
if (unified) {
val separator = ItemWidgetTimetableModel()
separator.profileId = profile.id
separator.bigStyle = widgetConfig.bigStyle
separator.darkTheme = widgetConfig.darkTheme
separator.separatorProfileName = profile.name
models.add(separator)
}
// search for lessons to display
val timetableDate = Date.getToday()
var checkedDays = 0
var lessons = lessonList.filter { it.profileId == profile.id && it.displayDate == timetableDate && it.type != Lesson.TYPE_NO_LESSONS }
while ((lessons.isEmpty() || lessons.none {
it.displayDate != today || (it.displayDate == today && it.displayEndTime != null && it.displayEndTime!! >= now)
}) && checkedDays < 7) {
timetableDate.stepForward(0, 0, 1)
lessons = lessonList.filter { it.profileId == profile.id && it.displayDate == timetableDate && it.type != Lesson.TYPE_NO_LESSONS }
checkedDays++
}
// set the displayingDate to show in the header
if (!unified) {
if (lessons.isNotEmpty())
displayingDate = timetableDate
profileId = profile.id
}
// get all events for the current date
val events = app.db.eventDao().getAllByDateNow(profile.id, timetableDate)?.filterNotNull() ?: emptyList()
lessons.forEachIndexed { pos, lesson ->
val model = ItemWidgetTimetableModel()
model.bigStyle = widgetConfig.bigStyle
model.darkTheme = widgetConfig.darkTheme
model.profileId = profile.id
model.lessonId = lesson.id
model.lessonDate = timetableDate
model.startTime = lesson.displayStartTime
model.endTime = lesson.displayEndTime
// check if the lesson has already passed or it's currently in progress
if (lesson.displayDate == today) {
lesson.displayEndTime?.let { endTime ->
model.lessonPassed = now > endTime
lesson.displayStartTime?.let { startTime ->
model.lessonCurrent = now in startTime..endTime
}
}
}
// set where should the list view scroll to
if (model.lessonCurrent) {
scrollPos = pos
} else if (model.lessonPassed) {
scrollPos = pos + 1
}
// set the subject and classroom name
model.subjectName = lesson.displaySubjectName
model.classroomName = lesson.displayClassroom
// set the bell sync to calculate progress in ListProvider
model.bellSyncDiffMillis = bellSyncDiffMillis
// make the model aware of the lesson type
when (lesson.type) {
Lesson.TYPE_CANCELLED -> {
model.lessonCancelled = true
}
Lesson.TYPE_CHANGE,
Lesson.TYPE_SHIFTED_SOURCE,
Lesson.TYPE_SHIFTED_TARGET -> {
model.lessonChange = true
}
}
// add every event on this lesson
for (event in events) {
if (event.startTime != null && event.startTime != lesson.displayStartTime)
continue
model.eventColors.add(if (event.type == TYPE_HOMEWORK) ItemWidgetTimetableModel.EVENT_COLOR_HOMEWORK else event.getColor())
}
models += model
}
}
if (unified) {
// set the title for an unified widget
views.setTextViewText(R.id.widgetTimetableTitle, app.getString(R.string.widget_timetable_title_unified))
views.setViewVisibility(R.id.widgetTimetableSubtitle, View.GONE)
} else {
// set the title to present the widget's profile
views.setTextViewText(R.id.widgetTimetableTitle, profileList[0].name)
views.setViewVisibility(R.id.widgetTimetableTitle, View.VISIBLE)
// make the subtitle show current date for these lessons
displayingDate?.let {
when (Date.diffDays(it, Date.getToday())) {
0 -> views.setTextViewText(R.id.widgetTimetableSubtitle, app.getString(R.string.day_today_format, Week.getFullDayName(it.weekDay)))
1 -> views.setTextViewText(R.id.widgetTimetableSubtitle, app.getString(R.string.day_tomorrow_format, Week.getFullDayName(it.weekDay)))
else -> views.setTextViewText(R.id.widgetTimetableSubtitle, Week.getFullDayName(it.weekDay) + " " + it.formattedString)
}
}
}
// intent running when the header is clicked
val openIntent = Intent(app, MainActivity::class.java)
openIntent.action = "android.intent.action.MAIN"
if (!unified) {
// per-profile widget should redirect to it + correct day
profileId?.let {
openIntent.putExtra("profileId", it)
}
displayingDate?.let {
openIntent.putExtra("timetableDate", it.value)
}
}
openIntent.putExtra("fragmentId", MainActivity.DRAWER_ITEM_TIMETABLE)
val pendingOpenIntent = PendingIntent.getActivity(app, appWidgetId, openIntent, 0)
views.setOnClickPendingIntent(R.id.widgetTimetableHeader, pendingOpenIntent)
if (lessonList.isEmpty()) {
views.setViewVisibility(R.id.widgetTimetableLoading, View.VISIBLE)
views.setRemoteAdapter(R.id.widgetTimetableListView, Intent())
views.setTextViewText(R.id.widgetTimetableLoading, app.getString(R.string.widget_timetable_no_lessons))
return
}
timetables!!.put(appWidgetId, models)
// apply the list service to the list view
val listIntent = Intent(app, WidgetTimetableService::class.java)
listIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
listIntent.data = Uri.parse(listIntent.toUri(Intent.URI_INTENT_SCHEME))
views.setRemoteAdapter(R.id.widgetTimetableListView, listIntent)
// create an intent used to display the lesson details dialog
val intentTemplate = Intent(app, LessonDialogActivity::class.java)
intentTemplate.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
val pendingIntentTimetable = PendingIntent.getActivity(app, appWidgetId, intentTemplate, 0)
views.setPendingIntentTemplate(R.id.widgetTimetableListView, pendingIntentTimetable)
if (!unified)
views.setScrollPosition(R.id.widgetTimetableListView, scrollPos)
}
override fun onEnabled(context: Context) {
// Enter relevant functionality for when the first widget is created
}
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
val app = context.applicationContext as App
for (appWidgetId in appWidgetIds) {
app.appConfig.widgetTimetableConfigs.remove(appWidgetId)
}
app.saveConfig("widgetTimetableConfigs")
}
companion object {
val ACTION_SYNC_DATA = "ACTION_SYNC_DATA"
private val TAG = "WidgetTimetable"
private val modeInt = 0
var timetables: SparseArray<List<ItemWidgetTimetableModel>>? = null
fun getPendingSelfIntent(context: Context, action: String): PendingIntent {
val intent = Intent(context, WidgetTimetable::class.java)
intent.action = action
return getPendingSelfIntent(context, intent)
}
fun getPendingSelfIntent(context: Context, intent: Intent): PendingIntent {
return PendingIntent.getBroadcast(context, 0, intent, 0)
}
fun drawableToBitmap(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
}
}

View File

@ -60,6 +60,9 @@ class ApiService : Service() {
private val notification by lazy { EdziennikNotification(this) }
private var lastEventTime = System.currentTimeMillis()
private var taskCancelTries = 0
/* ______ _ _ _ _ _____ _ _ _ _
| ____| | | (_) (_) | / ____| | | | | | |
| |__ __| |_____ ___ _ __ _ __ _| | __ | | __ _| | | |__ __ _ ___| | __
@ -68,22 +71,17 @@ class ApiService : Service() {
|______\__,_/___|_|\___|_| |_|_| |_|_|_|\_\ \_____\__,_|_|_|_.__/ \__,_|\___|_|\*/
private val taskCallback = object : EdziennikCallback {
override fun onCompleted() {
lastEventTime = System.currentTimeMillis()
d(TAG, "Task $taskRunningId (profile $taskProfileId) - $taskProgressText - finished")
//if (!taskCancelled) {
EventBus.getDefault().post(ApiTaskFinishedEvent(taskProfileId))
//}
taskIsRunning = false
taskRunningId = -1
taskRunning = null
taskProfileId = -1
taskProgress = -1f
taskProgressText = null
EventBus.getDefault().post(ApiTaskFinishedEvent(taskProfileId))
clearTask()
notification.setIdle().post()
runTask()
}
override fun onError(apiError: ApiError) {
lastEventTime = System.currentTimeMillis()
d(TAG, "Task $taskRunningId threw an error - $apiError")
apiError.profileId = taskProfileId
EventBus.getDefault().post(ApiTaskErrorEvent(apiError))
@ -92,9 +90,7 @@ class ApiService : Service() {
if (apiError.isCritical) {
taskRunning?.cancel()
notification.setCriticalError().post()
taskRunning = null
taskIsRunning = false
taskRunningId = -1
clearTask()
runTask()
}
else {
@ -103,6 +99,7 @@ class ApiService : Service() {
}
override fun onProgress(step: Float) {
lastEventTime = System.currentTimeMillis()
if (step <= 0)
return
if (taskProgress < 0)
@ -115,6 +112,7 @@ class ApiService : Service() {
}
override fun onStartProgress(stringRes: Int) {
lastEventTime = System.currentTimeMillis()
taskProgressText = getString(stringRes)
d(TAG, "Task $taskRunningId progress: $taskProgressText")
EventBus.getDefault().post(ApiTaskProgressEvent(taskProfileId, taskProgress, taskProgressText))
@ -129,6 +127,7 @@ class ApiService : Service() {
| | (_| \__ \ < | __/> < __/ (__| |_| | |_| | (_) | | | |
|_|\__,_|___/_|\_\ \___/_/\_\___|\___|\__,_|\__|_|\___/|_| |*/
private fun runTask() {
checkIfTaskFrozen()
if (taskIsRunning)
return
if (taskCancelled || serviceClosed || (taskQueue.isEmpty() && finishingTaskQueue.isEmpty())) {
@ -137,6 +136,8 @@ class ApiService : Service() {
return
}
lastEventTime = System.currentTimeMillis()
val task = if (taskQueue.isEmpty()) finishingTaskQueue.removeAt(0) else taskQueue.removeAt(0)
task.taskId = ++taskMaximumId
task.prepare(app)
@ -166,6 +167,48 @@ class ApiService : Service() {
}
}
/**
* Check if a task is inactive for more than 30 seconds.
* If the user tries to cancel a task with no success at least three times,
* consider it frozen as well.
*
* This usually means it is broken and won't become active again.
* This method cancels the task and removes any pointers to it.
*/
private fun checkIfTaskFrozen(): Boolean {
if (System.currentTimeMillis() - lastEventTime > 30*1000
|| taskCancelTries >= 3) {
val time = System.currentTimeMillis() - lastEventTime
d(TAG, "!!! Task $taskRunningId froze for $time ms. $taskRunning")
clearTask()
return true
}
return false
}
/**
* Stops the service if the current task is frozen/broken.
*/
private fun stopIfTaskFrozen() {
if (checkIfTaskFrozen()) {
stopSelf()
}
}
/**
* Remove any task descriptors or pointers from the service.
*/
private fun clearTask() {
taskIsRunning = false
taskRunningId = -1
taskRunning = null
taskProfileId = -1
taskProgress = -1f
taskProgressText = null
taskCancelled = false
taskCancelTries = 0
}
private fun allCompleted() {
EventBus.getDefault().post(ApiTaskAllFinishedEvent())
stopSelf()
@ -211,8 +254,10 @@ class ApiService : Service() {
EventBus.getDefault().removeStickyEvent(request)
d(TAG, request.toString())
taskCancelTries++
taskCancelled = true
taskRunning?.cancel()
stopIfTaskFrozen()
}
@Subscribe(sticky = true, threadMode = ThreadMode.ASYNC)
fun onServiceCloseRequest(request: ServiceCloseRequest) {

View File

@ -45,8 +45,8 @@ class DataNotifications(val data: Data) {
return@run
}
for (change in app.db.lessonChangeDao().getNotNotifiedNow(profileId)) {
val text = app.getString(R.string.notification_lesson_change_format, change.changeTypeStr(app), if (change.lessonDate == null) "" else change.lessonDate!!.formattedString, change.subjectLongName)
for (lesson in app.db.timetableDao().getNotNotifiedNow(profileId)) {
val text = app.getString(R.string.notification_lesson_change_format, lesson.getDisplayChangeType(app), if (lesson.displayDate == null) "" else lesson.displayDate!!.formattedString, lesson.changeSubjectName)
data.notifications += Notification(
title = app.getNotificationTitle(TYPE_TIMETABLE_LESSON_CHANGE),
text = text,
@ -54,8 +54,8 @@ class DataNotifications(val data: Data) {
profileId = profileId,
profileName = profileName,
viewId = DRAWER_ITEM_TIMETABLE,
addedDate = change.addedDate
).addExtra("timetableDate", change.lessonDate?.value?.toLong())
addedDate = lesson.addedDate
).addExtra("timetableDate", lesson.displayDate?.stringY_m_d ?: "")
}
for (event in app.db.eventDao().getNotNotifiedNow(profileId)) {
@ -186,10 +186,10 @@ class DataNotifications(val data: Data) {
val luckyNumbers = app.db.luckyNumberDao().getNotNotifiedNow(profileId)
luckyNumbers?.removeAll { it.date < today }
luckyNumbers?.forEach { luckyNumber ->
val text = when {
luckyNumber.date.value == todayValue -> // LN for today
val text = when (luckyNumber.date.value) {
todayValue -> // LN for today
app.getString(if (profile.studentNumber != -1 && profile.studentNumber == luckyNumber.number) R.string.notification_lucky_number_yours_format else R.string.notification_lucky_number_format, luckyNumber.number)
luckyNumber.date.value == todayValue + 1 -> // LN for tomorrow
todayValue + 1 -> // LN for tomorrow
app.getString(if (profile.studentNumber != -1 && profile.studentNumber == luckyNumber.number) R.string.notification_lucky_number_yours_tomorrow_format else R.string.notification_lucky_number_tomorrow_format, luckyNumber.number)
else -> // LN for later
app.getString(if (profile.studentNumber != -1 && profile.studentNumber == luckyNumber.number) R.string.notification_lucky_number_yours_later_format else R.string.notification_lucky_number_later_format, luckyNumber.date.formattedString, luckyNumber.number)
@ -207,4 +207,4 @@ class DataNotifications(val data: Data) {
data.db.metadataDao().setAllNotified(profileId, true)
}}
}
}

View File

@ -39,13 +39,16 @@ const val ERROR_REQUEST_HTTP_404 = 54
const val ERROR_REQUEST_HTTP_405 = 55
const val ERROR_REQUEST_HTTP_410 = 56
const val ERROR_REQUEST_HTTP_500 = 57
const val ERROR_REQUEST_FAILURE_HOSTNAME_NOT_FOUND = 60
const val ERROR_REQUEST_FAILURE_TIMEOUT = 61
const val ERROR_REQUEST_FAILURE_NO_INTERNET = 62
const val ERROR_RESPONSE_EMPTY = 100
const val ERROR_LOGIN_DATA_MISSING = 101
const val ERROR_LOGIN_DATA_INVALID = 102
const val ERROR_PROFILE_MISSING = 105
const val ERROR_INVALID_LOGIN_MODE = 110
const val ERROR_LOGIN_METHOD_NOT_SATISFIED = 111
const val ERROR_NOT_IMPLEMENTED = 112
const val ERROR_FILE_DOWNLOAD = 113
const val ERROR_NO_STUDENTS_IN_ACCOUNT = 115
@ -99,6 +102,13 @@ const val ERROR_LOGIN_LIBRUS_PORTAL_REFRESH_INVALID = 172
const val ERROR_LOGIN_LIBRUS_PORTAL_REFRESH_REVOKED = 173
const val ERROR_LIBRUS_SYNERGIA_OTHER = 174
const val ERROR_LIBRUS_SYNERGIA_MAINTENANCE = 175
const val ERROR_LIBRUS_MESSAGES_MAINTENANCE = 176
const val ERROR_LIBRUS_MESSAGES_ERROR = 177
const val ERROR_LIBRUS_MESSAGES_OTHER = 178
const val ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN = 179
const val ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN = 180
const val ERROR_LIBRUS_API_MAINTENANCE = 181
const val ERROR_LIBRUS_PORTAL_MAINTENANCE = 182
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_LOGIN = 201
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_OLD_PASSWORD = 202
@ -109,7 +119,7 @@ const val ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_ADDRESS = 206
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_OTHER = 210
const val ERROR_MOBIDZIENNIK_WEB_ACCESS_DENIED = 211
const val ERROR_MOBIDZIENNIK_WEB_NO_SESSION_KEY = 212
const val ERROR_MOBIDZIENNIK_WEB_NO_SESSION_VALUE = 212
const val ERROR_MOBIDZIENNIK_WEB_NO_SESSION_VALUE = 216
const val ERROR_MOBIDZIENNIK_WEB_NO_SERVER_ID = 213
const val ERROR_MOBIDZIENNIK_WEB_INVALID_RESPONSE = 214
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_NO_SESSION_ID = 215
@ -163,3 +173,5 @@ const val EXCEPTION_LIBRUS_MESSAGES_REQUEST = 911
const val EXCEPTION_IDZIENNIK_WEB_REQUEST = 912
const val EXCEPTION_IDZIENNIK_WEB_API_REQUEST = 913
const val EXCEPTION_IDZIENNIK_API_REQUEST = 914
const val LOGIN_NO_ARGUMENTS = 1201

View File

@ -7,39 +7,36 @@ package pl.szczodrzynski.edziennik.api.v2
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ANNOUNCEMENTS
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ATTENDANCE
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_BEHAVIOUR
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_GRADES
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOME
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOMEWORK
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_BEHAVIOUR
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT
const val FEATURE_ALL = 0
const val FEATURE_TIMETABLE = 1
const val FEATURE_AGENDA = 2
const val FEATURE_GRADES = 3
const val FEATURE_HOMEWORK = 4
const val FEATURE_BEHAVIOUR = 5
const val FEATURE_ATTENDANCE = 6
const val FEATURE_MESSAGES_INBOX = 7
const val FEATURE_MESSAGES_SENT = 8
const val FEATURE_ANNOUNCEMENTS = 9
internal const val FEATURE_TIMETABLE = 1
internal const val FEATURE_AGENDA = 2
internal const val FEATURE_GRADES = 3
internal const val FEATURE_HOMEWORK = 4
internal const val FEATURE_BEHAVIOUR = 5
internal const val FEATURE_ATTENDANCE = 6
internal const val FEATURE_MESSAGES_INBOX = 7
internal const val FEATURE_MESSAGES_SENT = 8
internal const val FEATURE_ANNOUNCEMENTS = 9
const val FEATURE_ALWAYS_NEEDED = 100
const val FEATURE_STUDENT_INFO = 101
const val FEATURE_STUDENT_NUMBER = 109
const val FEATURE_SCHOOL_INFO = 102
const val FEATURE_CLASS_INFO = 103
const val FEATURE_TEAM_INFO = 104
const val FEATURE_LUCKY_NUMBER = 105
const val FEATURE_TEACHERS = 106
const val FEATURE_SUBJECTS = 107
const val FEATURE_CLASSROOMS = 108
const val FEATURE_PUSH_CONFIG = 120
const val FEATURE_MESSAGE_GET = 201
internal const val FEATURE_ALWAYS_NEEDED = 100
internal const val FEATURE_STUDENT_INFO = 101
internal const val FEATURE_STUDENT_NUMBER = 109
internal const val FEATURE_SCHOOL_INFO = 102
internal const val FEATURE_CLASS_INFO = 103
internal const val FEATURE_TEAM_INFO = 104
internal const val FEATURE_LUCKY_NUMBER = 105
internal const val FEATURE_TEACHERS = 106
internal const val FEATURE_SUBJECTS = 107
internal const val FEATURE_CLASSROOMS = 108
internal const val FEATURE_PUSH_CONFIG = 120
object Features {
private fun getAllNecessary(): List<Int> = listOf(

View File

@ -37,6 +37,16 @@ object Regexes {
"""events: (.+),$""".toRegex(RegexOption.MULTILINE)
}
val MOBIDZIENNIK_MESSAGE_READ_DATE by lazy {
"""czas przeczytania:.+?,\s([0-9]+)\s(.+?)\s([0-9]{4}),\sgodzina\s([0-9:]+)""".toRegex(RegexOption.DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_MESSAGE_SENT_READ_DATE by lazy {
""".+?,\s([0-9]+)\s(.+?)\s([0-9]{4}),\sgodzina\s([0-9:]+)""".toRegex(RegexOption.DOT_MATCHES_ALL)
}
val MOBIDZIENNIK_MESSAGE_ATTACHMENT by lazy {
"""href="https://.+?\.mobidziennik.pl/.+?&(?:amp;)?zalacznik=([0-9]+)"(?:.+?<small.+?\(([0-9.]+)\s(M|K|G|)B\))*""".toRegex(RegexOption.DOT_MATCHES_ALL)
}
val IDZIENNIK_LOGIN_HIDDEN_FIELDS by lazy {
@ -60,4 +70,16 @@ object Regexes {
val IDZIENNIK_LOGIN_FIRST_STUDENT by lazy {
"""<option.*?value="([0-9]+)"\sdata-id-ucznia="([A-z0-9]+?)".*?>(.+?)\s(.+?)\s*\((.+?),\s*(.+?)\)</option>""".toRegex(RegexOption.DOT_MATCHES_ALL)
}
val VULCAN_SHITFT_ANNOTATION by lazy {
"""\(przeniesiona (z|na) lekcj[ię] ([0-9]+), (.+)\)""".toRegex()
}
val LIBRUS_ATTACHMENT_KEY by lazy {
"""singleUseKey=([0-9A-f_]+)""".toRegex()
}
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-11-24
*/
package pl.szczodrzynski.edziennik.api.v2.events
data class AttachmentGetEvent(val profileId: Int, val messageId: Long, val attachmentId: Long,
var eventType: Int = TYPE_PROGRESS, val fileName: String? = null,
val bytesWritten: Long = 0) {
companion object {
const val TYPE_PROGRESS = 0
const val TYPE_FINISHED = 1
}
}

View File

@ -24,6 +24,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
fun syncProfileList(profileList: List<Int>) = EdziennikTask(-1, SyncProfileListRequest(profileList))
fun messageGet(profileId: Int, message: MessageFull) = EdziennikTask(profileId, MessageGetRequest(message))
fun announcementsRead(profileId: Int) = EdziennikTask(profileId, AnnouncementsReadRequest())
fun attachmentGet(profileId: Int, messageId: Long, attachmentId: Long, attachmentName: String) = EdziennikTask(profileId, AttachmentGetRequest(messageId, attachmentId, attachmentName))
}
private lateinit var loginStore: LoginStore
@ -35,8 +36,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
loginStore = request.loginStore
// save the profile ID and name as the current task's
taskName = app.getString(R.string.edziennik_notification_api_first_login_title)
}
else {
} else {
// get the requested profile and login store
val profile = app.db.profileDao().getByIdNow(profileId)
this.profile = profile
@ -67,12 +67,14 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
when (request) {
is SyncProfileRequest -> edziennikInterface?.sync(
featureIds = request.viewIds?.flatMap { Features.getIdsByView(it.first, it.second) } ?: Features.getAllIds(),
featureIds = request.viewIds?.flatMap { Features.getIdsByView(it.first, it.second) }
?: Features.getAllIds(),
viewId = request.viewIds?.get(0)?.first,
arguments = request.arguments)
is MessageGetRequest -> edziennikInterface?.getMessage(request.message)
is FirstLoginRequest -> edziennikInterface?.firstLogin()
is AnnouncementsReadRequest -> edziennikInterface?.markAllAnnouncementsAsRead()
is AttachmentGetRequest -> edziennikInterface?.getAttachment(request.messageId, request.attachmentId, request.attachmentName)
}
}
@ -90,4 +92,5 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
data class SyncProfileListRequest(val profileList: List<Int>)
data class MessageGetRequest(val message: MessageFull)
class AnnouncementsReadRequest
data class AttachmentGetRequest(val messageId: Long, val attachmentId: Long, val attachmentName: String)
}

View File

@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.Notifier.ID_NOTIFICATIONS
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.db.modules.notification.getNotificationTitle
import pl.szczodrzynski.edziennik.utils.models.Notification
import kotlin.math.min
@ -33,9 +34,9 @@ class NotifyTask : IApiTask(-1) {
val pendingIntent = PendingIntent.getActivity(app, notification.id, intent, 0)
val notificationBuilder = NotificationCompat.Builder(app, app.notifier.notificationGroup)
// title, text, type, date
.setContentTitle(notification.title)
.setContentTitle(notification.profileName)
.setContentText(notification.text)
.setSubText(Notification.stringType(app, notification.type))
.setSubText(app.getNotificationTitle(notification.type))
.setWhen(notification.addedDate)
.setTicker(app.getString(R.string.notification_ticker_format, Notification.stringType(app, notification.type)))
// icon, color, lights, priority

View File

@ -138,10 +138,12 @@ class DataIdziennik(app: App, profile: Profile?, loginStore: LoginStore) : Data(
val teacher = teacherList.singleOrNull { it.fullName == "$firstName $lastName" }
return validateTeacher(teacher, firstName, lastName)
}
fun getTeacher(firstNameChar: Char, lastName: String): Teacher {
val teacher = teacherList.singleOrNull { it.shortName == "$firstNameChar.$lastName" }
return validateTeacher(teacher, firstNameChar.toString(), lastName)
}
fun getTeacherByLastFirst(nameLastFirst: String): Teacher {
val nameParts = nameLastFirst.split(" ")
return if (nameParts.size == 1) getTeacher(nameParts[0], "") else getTeacher(nameParts[1], nameParts[0])

View File

@ -70,6 +70,10 @@ class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore,
}
override fun getAttachment(messageId: Long, attachmentId: Long, attachmentName: String) {
}
override fun firstLogin() {
IdziennikFirstLogin(data) {
completed()

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-10-27.
* Copyright (c) Kacper Ziubryniewicz 2019-11-22
*/
package pl.szczodrzynski.edziennik.api.v2.idziennik.data.web
@ -12,33 +12,38 @@ import pl.szczodrzynski.edziennik.api.v2.idziennik.DataIdziennik
import pl.szczodrzynski.edziennik.api.v2.idziennik.ENDPOINT_IDZIENNIK_WEB_TIMETABLE
import pl.szczodrzynski.edziennik.api.v2.idziennik.data.IdziennikWeb
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.api.v2.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.modules.lessons.Lesson
import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange
import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange.TYPE_CANCELLED
import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonChange.TYPE_CHANGE
import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonRange
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
class IdziennikWebTimetable(override val data: DataIdziennik,
val onSuccess: () -> Unit) : IdziennikWeb(data) {
val onSuccess: () -> Unit) : IdziennikWeb(data) {
companion object {
private const val TAG = "IdziennikWebTimetable"
}
init {
val weekStart = Week.getWeekStart()
init { data.profile?.also { profile ->
val currentWeekStart = Week.getWeekStart()
if (Date.getToday().weekDay > 4) {
weekStart.stepForward(0, 0, 7)
currentWeekStart.stepForward(0, 0, 7)
}
val getDate = data.arguments?.getString("weekStart") ?: currentWeekStart.stringY_m_d
val weekStart = Date.fromY_m_d(getDate)
val weekEnd = weekStart.clone().stepForward(0, 0, 6)
webApiGet(TAG, IDZIENNIK_WEB_TIMETABLE, mapOf(
"idPozDziennika" to data.registerId,
"pidRokSzkolny" to data.schoolYearId,
"data" to weekStart.stringY_m_d+"T10:00:00.000Z"
"data" to "${weekStart.stringY_m_d}T10:00:00.000Z"
)) { result ->
val json = result.getJsonObject("d") ?: run {
data.error(ApiError(TAG, ERROR_IDZIENNIK_WEB_REQUEST_NO_DATA)
@ -56,73 +61,132 @@ class IdziennikWebTimetable(override val data: DataIdziennik,
data.lessonRanges[lessonRange.lessonNumber] = lessonRange
}
val dates = mutableSetOf<Int>()
val lessons = mutableListOf<Lesson>()
json.getJsonArray("Przedmioty")?.asJsonObjectList()?.forEach { lesson ->
val subject = data.getSubject(
lesson.getString("Nazwa") ?: return@forEach,
lesson.getLong("Id"),
lesson.getString("Skrot") ?: ""
)
val teacher = data.getTeacherByFDotLast(lesson.getString("Nauczyciel") ?: return@forEach)
val weekDay = lesson.getInt("DzienTygodnia")?.minus(1) ?: return@forEach
val lessonRange = data.lessonRanges[lesson.getInt("Godzina")?.plus(1) ?: return@forEach]
val teacher = data.getTeacherByFDotLast(lesson.getString("Nauczyciel")
?: return@forEach)
val lessonObject = Lesson(
profileId,
weekDay,
lessonRange.startTime,
lessonRange.endTime
).apply {
subjectId = subject.id
teacherId = teacher.id
teamId = data.teamClass?.id ?: -1
classroomName = lesson.getString("NazwaSali") ?: ""
val newSubjectName = lesson.getString("PrzedmiotZastepujacy")
val newSubject = when (newSubjectName.isNullOrBlank()) {
true -> null
else -> data.getSubject(newSubjectName, null, newSubjectName)
}
data.lessonList.add(lessonObject)
val newTeacherName = lesson.getString("NauZastepujacy")
val newTeacher = when (newTeacherName.isNullOrBlank()) {
true -> null
else -> data.getTeacherByFDotLast(newTeacherName)
}
val weekDay = lesson.getInt("DzienTygodnia")?.minus(1) ?: return@forEach
val lessonRange = data.lessonRanges[lesson.getInt("Godzina")?.plus(1)
?: return@forEach]
val lessonDate = weekStart.clone().stepForward(0, 0, weekDay)
val classroom = lesson.getString("NazwaSali")
val type = lesson.getInt("TypZastepstwa") ?: -1
if (type != -1) {
// we have a lesson change to process
val lessonChangeObject = LessonChange(
profileId,
weekStart.clone().stepForward(0, 0, weekDay),
lessonObject.startTime,
lessonObject.endTime
)
lessonChangeObject.teamId = lessonObject.teamId
lessonChangeObject.teacherId = lessonObject.teacherId
lessonChangeObject.subjectId = lessonObject.subjectId
lessonChangeObject.classroomName = lessonObject.classroomName
when (type) {
0 -> lessonChangeObject.type = TYPE_CANCELLED
1, 2, 3, 4, 5 -> {
lessonChangeObject.type = TYPE_CHANGE
val newTeacher = lesson.getString("NauZastepujacy")
val newSubject = lesson.getString("PrzedmiotZastepujacy")
if (newTeacher != null) {
lessonChangeObject.teacherId = data.getTeacherByFDotLast(newTeacher).id
}
if (newSubject != null) {
lessonChangeObject.subjectId = data.getSubject(newSubject, null, "").id
}
val lessonObject = Lesson(profileId, -1)
when (type) {
1, 2, 3, 4, 5 -> {
lessonObject.apply {
this.type = Lesson.TYPE_CHANGE
this.date = lessonDate
this.lessonNumber = lessonRange.lessonNumber
this.startTime = lessonRange.startTime
this.endTime = lessonRange.endTime
this.subjectId = newSubject?.id
this.teacherId = newTeacher?.id
this.teamId = data.teamClass?.id
this.classroom = classroom
this.oldDate = lessonDate
this.oldLessonNumber = lessonRange.lessonNumber
this.oldStartTime = lessonRange.startTime
this.oldEndTime = lessonRange.endTime
this.oldSubjectId = subject.id
this.oldTeacherId = teacher.id
this.oldTeamId = data.teamClass?.id
this.oldClassroom = classroom
}
}
0 -> {
lessonObject.apply {
this.type = Lesson.TYPE_CANCELLED
data.lessonChangeList.add(lessonChangeObject)
this.oldDate = lessonDate
this.oldLessonNumber = lessonRange.lessonNumber
this.oldStartTime = lessonRange.startTime
this.oldEndTime = lessonRange.endTime
this.oldSubjectId = subject.id
this.oldTeacherId = teacher.id
this.oldTeamId = data.teamClass?.id
this.oldClassroom = classroom
}
}
else -> {
lessonObject.apply {
this.type = Lesson.TYPE_NORMAL
this.date = lessonDate
this.lessonNumber = lessonRange.lessonNumber
this.startTime = lessonRange.startTime
this.endTime = lessonRange.endTime
this.subjectId = subject.id
this.teacherId = teacher.id
this.teamId = data.teamClass?.id
this.classroom = classroom
}
}
}
lessonObject.id = lessonObject.buildId()
dates.add(lessonDate.value)
lessons.add(lessonObject)
val seen = profile.empty || lessonDate < Date.getToday()
if (lessonObject.type != Lesson.TYPE_NORMAL && lessonDate >= Date.getToday()) {
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_LESSON_CHANGE,
lessonChangeObject.id,
profile?.empty ?: false,
profile?.empty ?: false,
lessonObject.id,
seen,
seen,
System.currentTimeMillis()
))
}
}
val date: Date = weekStart.clone()
while (date <= weekEnd) {
if (!dates.contains(date.value)) {
lessons.add(Lesson(profileId, date.value.toLong()).apply {
this.type = Lesson.TYPE_NO_LESSONS
this.date = date.clone()
})
}
date.stepForward(0, 0, 1)
}
d(TAG, "Clearing lessons between ${weekStart.stringY_m_d} and ${weekEnd.stringY_m_d} - timetable downloaded for $getDate")
data.lessonNewList.addAll(lessons)
data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd))
data.setSyncNext(ENDPOINT_IDZIENNIK_WEB_TIMETABLE, SYNC_ALWAYS)
onSuccess()
}
}
}}
}

View File

@ -11,6 +11,7 @@ interface EdziennikInterface {
fun sync(featureIds: List<Int>, viewId: Int? = null, arguments: JsonObject? = null)
fun getMessage(message: MessageFull)
fun markAllAnnouncementsAsRead()
fun getAttachment(messageId: Long, attachmentId: Long, attachmentName: String)
fun firstLogin()
fun cancel()
}

View File

@ -10,13 +10,11 @@ import pl.szczodrzynski.edziennik.api.v2.*
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusData
import pl.szczodrzynski.edziennik.api.v2.librus.data.messages.LibrusMessagesGetAttachment
import pl.szczodrzynski.edziennik.api.v2.librus.data.messages.LibrusMessagesGetMessage
import pl.szczodrzynski.edziennik.api.v2.librus.data.synergia.LibrusSynergiaMarkAllAnnouncementsAsRead
import pl.szczodrzynski.edziennik.api.v2.librus.firstlogin.LibrusFirstLogin
import pl.szczodrzynski.edziennik.api.v2.librus.login.LibrusLogin
import pl.szczodrzynski.edziennik.api.v2.librus.login.LibrusLoginApi
import pl.szczodrzynski.edziennik.api.v2.librus.login.LibrusLoginMessages
import pl.szczodrzynski.edziennik.api.v2.librus.login.LibrusLoginSynergia
import pl.szczodrzynski.edziennik.api.v2.librus.login.*
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
@ -82,11 +80,13 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
override fun getMessage(message: MessageFull) {
LibrusLoginApi(data) {
LibrusLoginSynergia(data) {
LibrusLoginMessages(data) {
LibrusMessagesGetMessage(data, message) {
completed()
LibrusLoginPortal(data) {
LibrusLoginApi(data) {
LibrusLoginSynergia(data) {
LibrusLoginMessages(data) {
LibrusMessagesGetMessage(data, message) {
completed()
}
}
}
}
@ -94,10 +94,26 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
override fun markAllAnnouncementsAsRead() {
LibrusLoginApi(data) {
LibrusLoginSynergia(data) {
LibrusSynergiaMarkAllAnnouncementsAsRead(data) {
completed()
LibrusLoginPortal(data) {
LibrusLoginApi(data) {
LibrusLoginSynergia(data) {
LibrusSynergiaMarkAllAnnouncementsAsRead(data) {
completed()
}
}
}
}
}
override fun getAttachment(messageId: Long, attachmentId: Long, attachmentName: String) {
LibrusLoginPortal(data) {
LibrusLoginApi(data) {
LibrusLoginSynergia(data) {
LibrusLoginMessages(data) {
LibrusMessagesGetAttachment(data, messageId, attachmentId, attachmentName) {
completed()
}
}
}
}
}

View File

@ -24,6 +24,7 @@ const val ENDPOINT_LIBRUS_API_DESCRIPTIVE_GC = 1023
const val ENDPOINT_LIBRUS_API_TEXT_GC = 1024
const val ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GC = 1025
const val ENDPOINT_LIBRUS_API_BEHAVIOUR_GC = 1026
const val ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS = 1030
const val ENDPOINT_LIBRUS_API_NORMAL_GRADES = 1031
const val ENDPOINT_LIBRUS_API_POINT_GRADES = 1032
const val ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES = 1033
@ -97,6 +98,7 @@ val LibrusFeatures = listOf(
ENDPOINT_LIBRUS_API_TEXT_GC to LOGIN_METHOD_LIBRUS_API,
ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GC to LOGIN_METHOD_LIBRUS_API,
ENDPOINT_LIBRUS_API_BEHAVIOUR_GC to LOGIN_METHOD_LIBRUS_API,
ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS to LOGIN_METHOD_LIBRUS_API,
ENDPOINT_LIBRUS_API_NORMAL_GRADES to LOGIN_METHOD_LIBRUS_API,
ENDPOINT_LIBRUS_API_POINT_GRADES to LOGIN_METHOD_LIBRUS_API,
ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES to LOGIN_METHOD_LIBRUS_API,

View File

@ -32,6 +32,13 @@ open class LibrusApi(open val data: DataLibrus) {
val callback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (response?.code() == HTTP_UNAVAILABLE) {
data.error(ApiError(tag, ERROR_LIBRUS_API_MAINTENANCE)
.withApiResponse(json)
.withResponse(response))
return
}
if (json == null && response?.parserErrorBody == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
@ -104,6 +111,7 @@ open class LibrusApi(open val data: DataLibrus) {
.allowErrorCode(HTTP_BAD_REQUEST)
.allowErrorCode(HTTP_FORBIDDEN)
.allowErrorCode(HTTP_UNAUTHORIZED)
.allowErrorCode(HTTP_UNAVAILABLE)
.callback(callback)
.build()
.enqueue()

View File

@ -85,6 +85,10 @@ class LibrusData(val data: DataLibrus, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_grades)
LibrusApiGrades(data, onSuccess)
}
ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS -> {
data.startProgress(R.string.edziennik_progress_endpoint_grade_comments)
LibrusApiGradeComments(data, onSuccess)
}
ENDPOINT_LIBRUS_API_NORMAL_GC -> {
data.startProgress(R.string.edziennik_progress_endpoint_grade_categories)
LibrusApiGradeCategories(data, onSuccess)

View File

@ -4,9 +4,12 @@
package pl.szczodrzynski.edziennik.api.v2.librus.data
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.body.MediaTypeUtils
import im.wangchao.mhttp.callback.FileCallbackHandler
import im.wangchao.mhttp.callback.JsonCallbackHandler
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.Cookie
import org.jsoup.Jsoup
@ -15,8 +18,8 @@ import org.jsoup.parser.Parser
import pl.szczodrzynski.edziennik.api.v2.*
import pl.szczodrzynski.edziennik.api.v2.librus.DataLibrus
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.utils.Utils.d
import java.io.File
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
@ -43,19 +46,19 @@ open class LibrusMessages(open val data: DataLibrus) {
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
if (text.isNullOrEmpty()) {
data.error(ApiError(LibrusSynergia.TAG, ERROR_RESPONSE_EMPTY)
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
return
}
// TODO: Finish error handling
if ("error" in text) {
when ("<type>(.*)</type>".toRegex().find(text)?.get(1)) {
"eAccessDeny" -> data.error(ApiError(tag, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED)
.withResponse(response)
.withApiResponse(text))
}
when {
text.contains("<message>Niepoprawny login i/lub hasło.</message>") -> data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN, response, text)
text.contains("stop.png") -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text)
text.contains("eAccessDeny") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text.contains("OffLine") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_MAINTENANCE, response, text)
text.contains("<status>error</status>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ERROR, response, text)
text.contains("<type>eVarWhitThisNameNotExists</type>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text.contains("<error>") -> data.error(TAG, ERROR_LIBRUS_MESSAGES_OTHER, response, text)
}
try {
@ -132,4 +135,95 @@ open class LibrusMessages(open val data: DataLibrus) {
.build()
.enqueue()
}
fun sandboxGet(tag: String, action: String, parameters: Map<String, Any>? = null,
onSuccess: (json: JsonObject) -> Unit) {
d(tag, "Request: Librus/Messages - $LIBRUS_SANDBOX_URL$action")
val callback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (json == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
return
}
try {
onSuccess(json)
} catch (e: Exception) {
data.error(ApiError(tag, EXCEPTION_LIBRUS_MESSAGES_REQUEST)
.withResponse(response)
.withThrowable(e)
.withApiResponse(json))
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url("$LIBRUS_SANDBOX_URL$action")
.userAgent(SYNERGIA_USER_AGENT)
.apply {
parameters?.forEach { (k, v) ->
addParameter(k, v)
}
}
.post()
.callback(callback)
.build()
.enqueue()
}
fun sandboxGetFile(tag: String, action: String, targetFile: File, onSuccess: (file: File) -> Unit,
onProgress: (written: Long, total: Long) -> Unit) {
d(tag, "Request: Librus/Messages - $LIBRUS_SANDBOX_URL$action")
val callback = object : FileCallbackHandler(targetFile) {
override fun onSuccess(file: File?, response: Response?) {
if (file == null) {
data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD)
.withResponse(response))
return
}
try {
onSuccess(file)
} catch (e: Exception) {
data.error(ApiError(tag, EXCEPTION_LIBRUS_MESSAGES_REQUEST)
.withResponse(response)
.withThrowable(e))
}
}
override fun onProgress(bytesWritten: Long, bytesTotal: Long) {
try {
onProgress(bytesWritten, bytesTotal)
} catch (e: Exception) {
data.error(ApiError(tag, EXCEPTION_LIBRUS_MESSAGES_REQUEST)
.withThrowable(e))
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url("$LIBRUS_SANDBOX_URL$action")
.userAgent(SYNERGIA_USER_AGENT)
.post()
.callback(callback)
.build()
.enqueue()
}
}

View File

@ -14,7 +14,7 @@ import pl.szczodrzynski.edziennik.utils.Utils.d
open class LibrusSynergia(open val data: DataLibrus) {
companion object {
const val TAG = "LibrusSynergia"
private const val TAG = "LibrusSynergia"
}
val profileId

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-11-20
*/
package pl.szczodrzynski.edziennik.api.v2.librus.data.api
import pl.szczodrzynski.edziennik.api.v2.librus.DataLibrus
import pl.szczodrzynski.edziennik.api.v2.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS
import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusApi
import pl.szczodrzynski.edziennik.asJsonObjectList
import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory
import pl.szczodrzynski.edziennik.getJsonArray
import pl.szczodrzynski.edziennik.getLong
import pl.szczodrzynski.edziennik.getString
class LibrusApiGradeComments(override val data: DataLibrus,
val onSuccess: () -> Unit) : LibrusApi(data) {
companion object {
const val TAG = "LibrusApiGradeComments"
}
init {
apiGet(TAG, "Grades/Comments") { json ->
json.getJsonArray("Comments")?.asJsonObjectList()?.forEach { comment ->
val id = comment.getLong("Id") ?: return@forEach
val text = comment.getString("Text")
val gradeCategoryObject = GradeCategory(
profileId,
id,
-1f,
-1,
text
).apply {
type = GradeCategory.TYPE_COMMENT
}
data.gradeCategories.put(id, gradeCategoryObject)
}
data.setSyncNext(ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS, SYNC_ALWAYS)
onSuccess()
}
}
}

View File

@ -6,6 +6,7 @@ import pl.szczodrzynski.edziennik.api.v2.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADE
import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusApi
import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.modules.grades.Grade
import pl.szczodrzynski.edziennik.data.db.modules.grades.GradeCategory
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
@ -42,12 +43,21 @@ class LibrusApiGrades(override val data: DataLibrus,
weight = 0f
}
val description = grade.getJsonArray("Comments")?.asJsonObjectList()?.let { comments ->
if (comments.isNotEmpty()) {
data.gradeCategories.singleOrNull {
it.type == GradeCategory.TYPE_COMMENT
&& it.categoryId == comments[0].asJsonObject.getLong("Id")
}?.text
} else null
} ?: ""
val gradeObject = Grade(
profileId,
id,
categoryName,
color,
"",
description,
name,
value,
weight,

View File

@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
class LibrusApiTimetables(override val data: DataLibrus,
val onSuccess: () -> Unit) : LibrusApi(data) {
@ -29,9 +30,18 @@ class LibrusApiTimetables(override val data: DataLibrus,
data.db.classroomDao().getAllNow(profileId).toSparseArray(data.classrooms) { it.id }
}
val currentWeekStart = Date.getToday().let { it.stepForward(0, 0, -it.weekDay) }
val currentWeekStart = Week.getWeekStart()
if (Date.getToday().weekDay > 4) {
currentWeekStart.stepForward(0, 0, 7)
}
val getDate = data.arguments?.getString("weekStart") ?: currentWeekStart.stringY_m_d
apiGet(TAG, "Timetables?weekStart=$getDate") { json ->
val weekStart = Date.fromY_m_d(getDate)
val weekEnd = weekStart.clone().stepForward(0, 0, 6)
apiGet(TAG, "Timetables?weekStart=${weekStart.stringY_m_d}") { json ->
val days = json.getJsonObject("Timetable")
days?.entrySet()?.forEach { (dateString, dayEl) ->
@ -44,21 +54,19 @@ class LibrusApiTimetables(override val data: DataLibrus,
val lessonRange = lessonRangeEl?.asJsonArray?.asJsonObjectList()
if (lessonRange?.isNullOrEmpty() == false)
lessonsFound = true
lessonRange?.forEachIndexed { index, lesson ->
lessonRange?.forEach { lesson ->
parseLesson(lessonDate, lesson)
}
}
if (day.isNullOrEmpty() || !lessonsFound) {
data.lessonNewList += Lesson(profileId, lessonDate.value.toLong()).apply {
data.lessonNewList.add(Lesson(profileId, lessonDate.value.toLong()).apply {
type = Lesson.TYPE_NO_LESSONS
date = lessonDate
}
})
}
}
val weekStart = Date.fromY_m_d(getDate)
val weekEnd = weekStart.clone().stepForward(0, 0, 6)
d(TAG, "Clearing lessons between ${weekStart.stringY_m_d} and ${weekEnd.stringY_m_d} - timetable downloaded for $getDate")
data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd))
@ -67,7 +75,7 @@ class LibrusApiTimetables(override val data: DataLibrus,
}
}
private fun parseLesson(lessonDate: Date, lesson: JsonObject) {
private fun parseLesson(lessonDate: Date, lesson: JsonObject) { data.profile?.also { profile ->
val isSubstitution = lesson.getBoolean("IsSubstitutionClass") ?: false
val isCancelled = lesson.getBoolean("IsCanceled") ?: false
@ -80,8 +88,7 @@ class LibrusApiTimetables(override val data: DataLibrus,
val virtualClassId = lesson.getJsonObject("VirtualClass")?.getLong("Id")
val teamId = lesson.getJsonObject("Class")?.getLong("Id") ?: virtualClassId
val id = lessonDate.combineWith(startTime) / 6L * 10L + (lesson.hashCode() and 0xFFFF)
val lessonObject = Lesson(profileId, id)
val lessonObject = Lesson(profileId, -1)
if (isSubstitution && isCancelled) {
// shifted lesson - source
@ -176,17 +183,21 @@ class LibrusApiTimetables(override val data: DataLibrus,
}
}
lessonObject.id = lessonObject.buildId()
val seen = profile.empty || lessonDate < Date.getToday()
if (lessonObject.type != Lesson.TYPE_NORMAL) {
data.metadataList.add(
Metadata(
data.profileId,
profileId,
Metadata.TYPE_LESSON_CHANGE,
lessonObject.id,
data.profile?.empty ?: false,
data.profile?.empty ?: false,
seen,
seen,
System.currentTimeMillis()
))
}
data.lessonNewList.add(lessonObject)
}
}}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-11-24
*/
package pl.szczodrzynski.edziennik.api.v2.librus.data.messages
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.api.v2.ERROR_FILE_DOWNLOAD
import pl.szczodrzynski.edziennik.api.v2.EXCEPTION_LIBRUS_MESSAGES_REQUEST
import pl.szczodrzynski.edziennik.api.v2.Regexes
import pl.szczodrzynski.edziennik.api.v2.events.AttachmentGetEvent
import pl.szczodrzynski.edziennik.api.v2.events.AttachmentGetEvent.Companion.TYPE_FINISHED
import pl.szczodrzynski.edziennik.api.v2.events.AttachmentGetEvent.Companion.TYPE_PROGRESS
import pl.szczodrzynski.edziennik.api.v2.librus.DataLibrus
import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusMessages
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.Utils
import java.io.File
import kotlin.coroutines.CoroutineContext
class LibrusMessagesGetAttachment(override val data: DataLibrus, val messageId: Long, val attachmentId: Long,
val attachmentName: String, val onSuccess: () -> Unit) : LibrusMessages(data), CoroutineScope {
companion object {
const val TAG = "LibrusMessagesGetAttachment"
}
private var job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Default
private var getAttachmentCheckKeyTries = 0
init {
messagesGet(TAG, "GetFileDownloadLink", parameters = mapOf(
"fileId" to attachmentId,
"msgId" to messageId,
"archive" to 0
)) { doc ->
val downloadLink = doc.select("response GetFileDownloadLink downloadLink").text()
val keyMatcher = Regexes.LIBRUS_ATTACHMENT_KEY.find(downloadLink)
if (keyMatcher != null) {
getAttachmentCheckKeyTries = 0
val attachmentKey = keyMatcher[1]
getAttachmentCheckKey(attachmentKey) {
downloadAttachment(attachmentKey)
}
} else {
data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD)
.withApiResponse(doc.toString()))
}
onSuccess()
}
}
private fun getAttachmentCheckKey(attachmentKey: String, callback: () -> Unit) {
sandboxGet(TAG, "CSCheckKey",
parameters = mapOf("singleUseKey" to attachmentKey)) { json ->
when (json.getString("status")) {
"not_downloaded_yet" -> {
if (getAttachmentCheckKeyTries++ > 5) {
data.error(ApiError(TAG, ERROR_FILE_DOWNLOAD)
.withApiResponse(json))
return@sandboxGet
}
launch {
delay(2000)
getAttachmentCheckKey(attachmentKey, callback)
}
}
"ready" -> {
launch { callback() }
}
else -> {
data.error(ApiError(TAG, EXCEPTION_LIBRUS_MESSAGES_REQUEST)
.withApiResponse(json))
}
}
}
}
private fun downloadAttachment(attachmentKey: String) {
val targetFile = File(Utils.getStorageDir(), attachmentName)
sandboxGetFile(TAG, "CSDownload&singleUseKey=$attachmentKey",
targetFile, { file ->
val event = AttachmentGetEvent(
profileId,
messageId,
attachmentId,
TYPE_FINISHED,
file.absolutePath
)
val attachmentDataFile = File(Utils.getStorageDir(), ".${profileId}_${event.messageId}_${event.attachmentId}")
Utils.writeStringToFile(attachmentDataFile, event.fileName)
EventBus.getDefault().post(event)
}) { written, _ ->
val event = AttachmentGetEvent(
profileId,
messageId,
attachmentId,
TYPE_PROGRESS,
bytesWritten = written
)
EventBus.getDefault().post(event)
}
}
}

View File

@ -13,6 +13,7 @@ import pl.szczodrzynski.edziennik.api.v2.librus.ENDPOINT_LIBRUS_MESSAGES_SENT
import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusMessages
import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
@ -20,7 +21,7 @@ import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
class LibrusMessagesGetList(override val data: DataLibrus, private val type: Int = Message.TYPE_RECEIVED,
class LibrusMessagesGetList(override val data: DataLibrus, private val type: Int = TYPE_RECEIVED,
archived: Boolean = false, val onSuccess: () -> Unit) : LibrusMessages(data) {
companion object {
const val TAG = "LibrusMessagesGetList"
@ -28,7 +29,7 @@ class LibrusMessagesGetList(override val data: DataLibrus, private val type: Int
init {
val endpoint = when (type) {
Message.TYPE_RECEIVED -> "Inbox/action/GetList"
TYPE_RECEIVED -> "Inbox/action/GetList"
Message.TYPE_SENT -> "Outbox/action/GetList"
else -> null
}
@ -46,34 +47,38 @@ class LibrusMessagesGetList(override val data: DataLibrus, private val type: Int
else -> 0
}
val sentDate = Date.fromIso(element.select("sendDate").text().trim())
var senderId: Long = -1
var receiverId: Long = -1
when (type) {
Message.TYPE_RECEIVED -> {
val senderFirstName = element.select("senderFirstName").text().trim()
val senderLastName = element.select("senderLastName").text().trim()
senderId = data.teacherList.singleOrNull {
it.name == senderFirstName && it.surname == senderLastName
}?.id ?: -1
}
val recipientFirstName = element.select(when (type) {
TYPE_RECEIVED -> "senderFirstName"
else -> "receiverFirstName"
}).text().trim()
Message.TYPE_SENT -> {
val receiverFirstName = element.select("receiverFirstName").text().trim()
val receiverLastName = element.select("receiverLastName").text().trim()
receiverId = data.teacherList.singleOrNull {
it.name == receiverFirstName && it.surname == receiverLastName
}?.id ?: {
val teacherObject = Teacher(
profileId,
-1 * Utils.crc16("$receiverFirstName $receiverLastName".toByteArray()).toLong(),
receiverFirstName,
receiverLastName
)
data.teacherList.put(teacherObject.id, teacherObject)
teacherObject.id
}.invoke()
}
val recipientLastName = element.select(when (type) {
TYPE_RECEIVED -> "senderLastName"
else -> "receiverLastName"
}).text().trim()
val recipientId = data.teacherList.singleOrNull {
it.name == recipientFirstName && it.surname == recipientLastName
}?.id ?: {
val teacherObject = Teacher(
profileId,
-1 * Utils.crc16("$recipientFirstName $recipientLastName".toByteArray()).toLong(),
recipientFirstName,
recipientLastName
)
data.teacherList.put(teacherObject.id, teacherObject)
teacherObject.id
}.invoke()
val senderId = when (type) {
TYPE_RECEIVED -> recipientId
else -> -1
}
val receiverId = when (type) {
TYPE_RECEIVED -> -1
else -> recipientId
}
val notified = when (type) {
@ -112,7 +117,7 @@ class LibrusMessagesGetList(override val data: DataLibrus, private val type: Int
}
when (type) {
Message.TYPE_RECEIVED -> data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_RECEIVED, SYNC_ALWAYS)
TYPE_RECEIVED -> data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_RECEIVED, SYNC_ALWAYS)
Message.TYPE_SENT -> data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_SENT, DAY, DRAWER_ITEM_MESSAGES)
}
onSuccess()

View File

@ -16,8 +16,7 @@ import pl.szczodrzynski.edziennik.getInt
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.getUnixDate
import pl.szczodrzynski.edziennik.utils.Utils.d
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
import java.net.HttpURLConnection.HTTP_UNAUTHORIZED
import java.net.HttpURLConnection.*
class LibrusLoginApi {
companion object {
@ -117,6 +116,13 @@ class LibrusLoginApi {
private val tokenCallback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (response?.code() == HTTP_UNAVAILABLE) {
data.error(ApiError(TAG, ERROR_LIBRUS_API_MAINTENANCE)
.withApiResponse(json)
.withResponse(response))
return
}
if (json == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
@ -176,6 +182,7 @@ class LibrusLoginApi {
.post()
.allowErrorCode(HTTP_BAD_REQUEST)
.allowErrorCode(HTTP_UNAUTHORIZED)
.allowErrorCode(HTTP_UNAVAILABLE)
.callback(tokenCallback)
.build()
.enqueue()

View File

@ -6,20 +6,57 @@ package pl.szczodrzynski.edziennik.api.v2.librus.login
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.body.MediaTypeUtils
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.Cookie
import pl.szczodrzynski.edziennik.api.v2.*
import pl.szczodrzynski.edziennik.api.v2.librus.DataLibrus
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.getUnixDate
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.d
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "LoginLibrusMessages"
}
private val callback by lazy { object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
val location = response?.headers()?.get("Location")
when {
location?.contains("MultiDomainLogon") == true -> loginWithSynergia(location)
location?.contains("AutoLogon") == true -> {
saveSessionId(response, text)
onSuccess()
}
text?.contains("<status>ok</status>") == true -> {
saveSessionId(response, text)
onSuccess()
}
text?.contains("<message>Niepoprawny login i/lub hasło.</message>") == true -> data.error(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_INVALID_LOGIN, response, text)
text?.contains("stop.png") == true -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text)
text?.contains("eAccessDeny") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text?.contains("OffLine") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_MAINTENANCE, response, text)
text?.contains("<status>error</status>") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ERROR, response, text)
text?.contains("<type>eVarWhitThisNameNotExists</type>") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text?.contains("<error>") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_OTHER, response, text)
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(TAG, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}}
init { run {
if (data.profile == null) {
data.error(ApiError(TAG, ERROR_PROFILE_MISSING))
@ -41,7 +78,7 @@ class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
if (data.loginMethods.contains(LOGIN_METHOD_LIBRUS_SYNERGIA)) {
loginWithSynergia()
}
else if (data.apiLogin != null && data.apiPassword != null && false) {
else if (data.apiLogin != null && data.apiPassword != null) {
loginWithCredentials()
}
else {
@ -54,7 +91,44 @@ class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
* XML (Flash messages website) login method. Uses a Synergia login and password.
*/
private fun loginWithCredentials() {
d(TAG, "Request: Librus/Login/Messages - $LIBRUS_MESSAGES_URL/Login")
val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
val doc = docBuilder.newDocument()
val serviceElement = doc.createElement("service")
val headerElement = doc.createElement("header")
val dataElement = doc.createElement("data")
val loginElement = doc.createElement("login")
loginElement.appendChild(doc.createTextNode(data.apiLogin))
dataElement.appendChild(loginElement)
val passwordElement = doc.createElement("login")
passwordElement.appendChild(doc.createTextNode(data.apiPassword))
dataElement.appendChild(passwordElement)
val keyStrokeElement = doc.createElement("KeyStroke")
val keysElement = doc.createElement("Keys")
val upElement = doc.createElement("Up")
keysElement.appendChild(upElement)
val downElement = doc.createElement("Down")
keysElement.appendChild(downElement)
keyStrokeElement.appendChild(keysElement)
dataElement.appendChild(keyStrokeElement)
serviceElement.appendChild(headerElement)
serviceElement.appendChild(dataElement)
doc.appendChild(serviceElement)
val transformer = TransformerFactory.newInstance().newTransformer()
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
val stringWriter = StringWriter()
transformer.transform(DOMSource(doc), StreamResult(stringWriter))
val requestXml = stringWriter.toString()
Request.builder()
.url("$LIBRUS_MESSAGES_URL/Login")
.userAgent(SYNERGIA_USER_AGENT)
.setTextBody(requestXml, MediaTypeUtils.APPLICATION_XML)
.post()
.callback(callback)
.build()
.enqueue()
}
/**
@ -63,37 +137,6 @@ class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
private fun loginWithSynergia(url: String = "https://synergia.librus.pl/wiadomosci2") {
d(TAG, "Request: Librus/Login/Messages - $url")
val callback = object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response?) {
val location = response?.headers()?.get("Location")
when {
location?.contains("MultiDomainLogon") == true -> loginWithSynergia(location)
location?.contains("AutoLogon") == true -> {
var sessionId = data.app.cookieJar.getCookie("wiadomosci.librus.pl", "DZIENNIKSID")
sessionId = sessionId?.replace("-MAINT", "")
if (sessionId == null) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_NO_SESSION_ID)
.withResponse(response)
.withApiResponse(text))
return
}
data.messagesSessionId = sessionId
data.messagesSessionIdExpiryTime = response.getUnixDate() + 45 * 60 /* 45min */
onSuccess()
}
text?.contains("eAccessDeny") == true -> data.error(TAG, ERROR_LIBRUS_MESSAGES_ACCESS_DENIED, response, text)
text?.contains("stop.png") == true -> data.error(TAG, ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED, response, text)
}
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(TAG, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url(url)
.userAgent(SYNERGIA_USER_AGENT)
@ -103,4 +146,17 @@ class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) {
.build()
.enqueue()
}
private fun saveSessionId(response: Response?, text: String?) {
var sessionId = data.app.cookieJar.getCookie("wiadomosci.librus.pl", "DZIENNIKSID")
sessionId = sessionId?.replace("-MAINT", "") // dunno what's this
if (sessionId == null) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_MESSAGES_NO_SESSION_ID)
.withResponse(response)
.withApiResponse(text))
return
}
data.messagesSessionId = sessionId
data.messagesSessionIdExpiryTime = response.getUnixDate() + 45 * 60 /* 45min */
}
}

View File

@ -99,6 +99,14 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
.post()
.callback(object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response) {
val location = response.headers()?.get("Location")
if (location == "http://localhost/bar?command=close") {
data.error(ApiError(TAG, ERROR_LIBRUS_PORTAL_MAINTENANCE)
.withApiResponse(json)
.withResponse(response))
return
}
if (json == null) {
if (response.parserErrorBody?.contains("wciąż nieaktywne") == true) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED)
@ -120,7 +128,7 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
override fun onFailure(response: Response, throwable: Throwable) {
if (response.code() == 403 || response.code() == 401) {
data.error(ApiError(TAG, ERROR_LOGIN_DATA_INVALID)
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN)
.withResponse(response)
.withThrowable(throwable))
return

View File

@ -7,7 +7,6 @@ package pl.szczodrzynski.edziennik.api.v2.librus.login
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.JsonCallbackHandler
import im.wangchao.mhttp.callback.TextCallbackHandler
import okhttp3.Cookie
import pl.szczodrzynski.edziennik.api.v2.*
@ -16,7 +15,6 @@ import pl.szczodrzynski.edziennik.api.v2.librus.data.LibrusApi
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.getUnixDate
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.d
import java.net.HttpURLConnection
@ -86,6 +84,13 @@ class LibrusLoginSynergia(override val data: DataLibrus, val onSuccess: () -> Un
val callback = object : TextCallbackHandler() {
override fun onSuccess(json: String?, response: Response?) {
val location = response?.headers()?.get("Location")
if (location?.endsWith("przerwa_techniczna") == true) {
data.error(ApiError(TAG, ERROR_LIBRUS_SYNERGIA_MAINTENANCE)
.withApiResponse(json)
.withResponse(response))
return
}
if (location?.endsWith("centrum_powiadomien") == true) {
val sessionId = data.app.cookieJar.getCookie("synergia.librus.pl", "DZIENNIKSID")
if (sessionId == null) {

View File

@ -10,8 +10,10 @@ import pl.szczodrzynski.edziennik.api.v2.CODE_INTERNAL_LIBRUS_ACCOUNT_410
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.data.MobidziennikData
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.data.web.MobidziennikWebGetMessage
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.firstlogin.MobidziennikFirstLogin
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.login.MobidziennikLogin
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.login.MobidziennikLoginWeb
import pl.szczodrzynski.edziennik.api.v2.mobidziennikLoginMethods
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.api.v2.prepare
@ -63,13 +65,21 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto
}
override fun getMessage(message: MessageFull) {
MobidziennikLoginWeb(data) {
MobidziennikWebGetMessage(data, message) {
completed()
}
}
}
override fun markAllAnnouncementsAsRead() {
}
override fun getAttachment(messageId: Long, attachmentId: Long, attachmentName: String) {
}
override fun firstLogin() {
MobidziennikFirstLogin(data) {
completed()

View File

@ -4,7 +4,6 @@
package pl.szczodrzynski.edziennik.api.v2.mobidziennik.data.api
import pl.szczodrzynski.edziennik.App.profileId
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.data.db.modules.teams.Team
import pl.szczodrzynski.edziennik.getById
@ -25,7 +24,7 @@ class MobidziennikApiTeams(val data: DataMobidziennik, tableTeams: List<String>?
val teacherId = cols[4].toLongOrNull() ?: -1
val teamObject = Team(
profileId,
data.profileId,
id,
name,
type,

View File

@ -4,7 +4,6 @@
package pl.szczodrzynski.edziennik.api.v2.mobidziennik.data.api
import pl.szczodrzynski.edziennik.App.profileId
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.api.v2.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
@ -15,7 +14,7 @@ import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
init {
init { data.profile?.also { profile ->
val lessons = rows.filterNot { it.isEmpty() }.map { it.split("|") }
val dataStart = Date.getToday()
@ -33,7 +32,6 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
val date = Date.fromYmd(lesson[2])
val startTime = Time.fromYmdHm(lesson[3])
val endTime = Time.fromYmdHm(lesson[4])
val id = date.combineWith(startTime) / 6L * 10L + (lesson.joinToString("|").hashCode() and 0xFFFF)
dataDays.remove(date.value)
@ -42,7 +40,7 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
val teamId = data.teamList.singleOrNull { it.name == lesson[8]+lesson[9] }?.id ?: -1
val classroom = lesson[11]
Lesson(data.profileId, id).also {
Lesson(data.profileId, -1).also {
when (lesson[1]) {
"plan_lekcji", "lekcja" -> {
it.type = Lesson.TYPE_NORMAL
@ -76,14 +74,18 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
}
}
it.id = it.buildId()
val seen = profile.empty || date < Date.getToday()
if (it.type != Lesson.TYPE_NORMAL) {
data.metadataList.add(
Metadata(
data.profileId,
Metadata.TYPE_LESSON_CHANGE,
it.id,
data.profile?.empty ?: false,
data.profile?.empty ?: false,
seen,
seen,
System.currentTimeMillis()
))
}
@ -93,7 +95,7 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
for (day in dataDays) {
val lessonDate = Date.fromValue(day)
data.lessonNewList += Lesson(profileId, lessonDate.value.toLong()).apply {
data.lessonNewList += Lesson(data.profileId, lessonDate.value.toLong()).apply {
type = Lesson.TYPE_NO_LESSONS
date = lessonDate
}
@ -195,5 +197,5 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
}
}
}*/
}
}
}}
}

View File

@ -0,0 +1,157 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-18.
*/
package pl.szczodrzynski.edziennik.api.v2.mobidziennik.data.web
import org.greenrobot.eventbus.EventBus
import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.api.v2.Regexes
import pl.szczodrzynski.edziennik.api.v2.events.MessageGetEvent
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.api.v2.mobidziennik.data.MobidziennikWeb
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipientFull
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.utils.Utils.monthFromName
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
class MobidziennikWebGetMessage(
override val data: DataMobidziennik,
private val message: MessageFull,
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
companion object {
private const val TAG = "MobidziennikWebGetMessage"
}
init {
val typeUrl = if (message.type == Message.TYPE_SENT)
"wiadwyslana"
else
"wiadodebrana"
webGet(TAG, "/dziennik/$typeUrl/?id=${message.id}") { text ->
MobidziennikLuckyNumberExtractor(data, text)
val messageRecipientList = mutableListOf<MessageRecipientFull>()
val doc = Jsoup.parse(text)
val content = doc.select("#content").first()
val body = content.select(".wiadomosc_tresc").first()
if (message.type == TYPE_RECEIVED) {
var readDate = System.currentTimeMillis()
Regexes.MOBIDZIENNIK_MESSAGE_READ_DATE.find(body.html())?.let {
val date = Date(
it[3].toIntOrNull() ?: 2019,
monthFromName(it[2]),
it[1].toIntOrNull() ?: 1
)
val time = Time.fromH_m_s(
it[4] // TODO blank string safety
)
readDate = date.combineWith(time)
}
val recipient = MessageRecipientFull(
profileId,
-1,
-1,
readDate,
message.id
)
recipient.fullName = profile?.accountNameLong ?: profile?.studentNameLong
messageRecipientList.add(recipient)
} else {
message.senderId = -1
message.senderReplyId = -1
content.select("table.spis tr:has(td)")?.forEach { recipientEl ->
val senderEl = recipientEl.select("td:eq(0)").first()
val senderName = senderEl.text()
val teacher = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName }
val receiverId = teacher?.id ?: -1
var readDate = 0L
val isReadEl = recipientEl.select("td:eq(2)").first()
if (isReadEl.ownText() != "NIE") {
val readDateEl = recipientEl.select("td:eq(3) small").first()
Regexes.MOBIDZIENNIK_MESSAGE_SENT_READ_DATE.find(readDateEl.ownText())?.let {
val date = Date(
it[3].toIntOrNull() ?: 2019,
monthFromName(it[2]),
it[1].toIntOrNull() ?: 1
)
val time = Time.fromH_m_s(
it[4] // TODO blank string safety
)
readDate = date.combineWith(time)
}
}
val recipient = MessageRecipientFull(
profileId,
receiverId,
-1,
readDate,
message.id
)
recipient.fullName = teacher?.fullName ?: "?"
messageRecipientList.add(recipient)
}
}
// this line removes the sender and read date details
body.select("div").remove()
// this needs to be at the end
message.apply {
this.body = body.html()
clearAttachments()
content.select("ul li").map { it.select("a").first() }.forEach {
val attachmentName = it.ownText()
Regexes.MOBIDZIENNIK_MESSAGE_ATTACHMENT.find(it.outerHtml())?.let { match ->
val attachmentId = match[1].toLong()
var size = match[2].toFloatOrNull() ?: 0f
when (match[3]) {
"K" -> size *= 1024f
"M" -> size *= 1024f * 1024f
"G" -> size *= 1024f * 1024f * 1024f
}
message.addAttachment(attachmentId, attachmentName, size.toLong())
}
}
}
if (!message.seen) { // TODO discover why this monstrosity instead of MetadataDao.setSeen
data.messageMetadataList.add(Metadata(
message.profileId,
Metadata.TYPE_MESSAGE,
message.id,
true,
true,
message.addedDate
))
}
message.recipients = messageRecipientList
data.messageRecipientList.addAll(messageRecipientList)
data.messageList.add(message)
EventBus.getDefault().postSticky(MessageGetEvent(message))
onSuccess()
}
}
}

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.api.v2.models
import android.content.Context
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
@ -52,6 +53,15 @@ class ApiError(val tag: String, val errorCode: Int) {
)
}
fun getStringReason(context: Context): String {
return context.resources.getIdentifier("error_${errorCode}_reason", "string", context.packageName).let {
if (it != 0)
context.getString(it)
else
"?"
}
}
override fun toString(): String {
return "ApiError(tag='$tag', errorCode=$errorCode, profileId=$profileId, throwable=$throwable, apiResponse=$apiResponse, request=$request, response=$response, isCritical=$isCritical)"
}

View File

@ -375,7 +375,7 @@ open class Data(val app: App, val profile: Profile?, val loginStore: LoginStore)
}
fun shouldSyncLuckyNumber(): Boolean {
return (db.luckyNumberDao().getNearestFutureNow(profileId, Date.getToday()) ?: -1) == -1
return (db.luckyNumberDao().getNearestFutureNow(profileId, Date.getToday().value) ?: -1) == -1
}
fun error(tag: String, errorCode: Int, response: Response? = null, throwable: Throwable? = null, apiResponse: JsonObject? = null) {

View File

@ -70,6 +70,10 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore,
}
override fun getAttachment(messageId: Long, attachmentId: Long, attachmentName: String) {
}
override fun firstLogin() {
TemplateFirstLogin(data) {
completed()

View File

@ -7,15 +7,14 @@ package pl.szczodrzynski.edziennik.api.v2.vulcan
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.api.v2.LOGIN_METHOD_VULCAN_API
import pl.szczodrzynski.edziennik.api.v2.models.Data
import pl.szczodrzynski.edziennik.currentTimeUnix
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
fun isApiLoginValid() = apiCertificateExpiryTime-30 > currentTimeUnix()
&& apiCertificateKey.isNotNullNorEmpty()
fun isApiLoginValid() = /*apiCertificateExpiryTime-30 > currentTimeUnix()
&&*/ apiCertificateKey.isNotNullNorEmpty()
&& apiCertificatePrivate.isNotNullNorEmpty()
&& symbol.isNotNullNorEmpty()
@ -165,6 +164,8 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
"GD1" -> "https://uonetplus-komunikacja.edu.gdansk.pl"
"KA1" -> "https://uonetplus-komunikacja.mcuw.katowice.eu"
"KA2" -> "https://uonetplus-komunikacja-test.mcuw.katowice.eu"
"LU1" -> "https://uonetplus-komunikacja.edu.lublin.eu"
"LU2" -> "https://test-uonetplus-komunikacja.edu.lublin.eu"
"P03" -> "https://efeb-komunikacja-pro-efebmobile.pro.vulcan.pl"
"P01" -> "http://efeb-komunikacja.pro-hudson.win.vulcan.pl"
"P02" -> "http://efeb-komunikacja.pro-hudsonrc.win.vulcan.pl"
@ -173,7 +174,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
"SZ9" -> "http://vulcan.szkolny.eu"
else -> null
}
return if (url != null) "$url/$symbol" else null
return if (url != null) "$url/$symbol" else loginStore.getLoginData("apiUrl", null)
}
val fullApiUrl: String?

View File

@ -12,8 +12,10 @@ import pl.szczodrzynski.edziennik.api.v2.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.edziennik.api.v2.prepare
import pl.szczodrzynski.edziennik.api.v2.vulcan.data.VulcanData
import pl.szczodrzynski.edziennik.api.v2.vulcan.data.api.VulcanApiMessagesChangeStatus
import pl.szczodrzynski.edziennik.api.v2.vulcan.firstlogin.VulcanFirstLogin
import pl.szczodrzynski.edziennik.api.v2.vulcan.login.VulcanLogin
import pl.szczodrzynski.edziennik.api.v2.vulcan.login.VulcanLoginApi
import pl.szczodrzynski.edziennik.api.v2.vulcanLoginMethods
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
@ -63,13 +65,21 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
}
override fun getMessage(message: MessageFull) {
VulcanLoginApi(data) {
VulcanApiMessagesChangeStatus(data, message) {
completed()
}
}
}
override fun markAllAnnouncementsAsRead() {
}
override fun getAttachment(messageId: Long, attachmentId: Long, attachmentName: String) {
}
override fun firstLogin() {
VulcanFirstLogin(data) {
completed()

View File

@ -64,6 +64,10 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_attendance)
VulcanApiAttendance(data, onSuccess)
}
ENDPOINT_VULCAN_API_TIMETABLE -> {
data.startProgress(R.string.edziennik_progress_endpoint_timetable)
VulcanApiTimetable(data, onSuccess)
}
ENDPOINT_VULCAN_API_MESSAGES_INBOX -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox)
VulcanApiMessagesInbox(data, onSuccess)

View File

@ -55,7 +55,7 @@ class VulcanApiAttendance(override val data: DataVulcan, val onSuccess: () -> Un
lessonSemester,
attendance.getString("PrzedmiotNazwa") + attendanceCategory.name.let { " - $it" },
lessonDate,
data.lessonRanges.get(attendance.getInt("IdPoraLekcji") ?: 0)?.startTime,
data.lessonRanges.get(attendance.getInt("Numer") ?: 0)?.startTime,
type)
data.attendanceList.add(attendanceObject)

View File

@ -35,7 +35,7 @@ class VulcanApiDictionaries(override val data: DataVulcan, val onSuccess: () ->
elements?.getJsonArray("KategorieUwag")?.forEach { saveNoticeType(it.asJsonObject) }
elements?.getJsonArray("KategorieFrekwencji")?.forEach { saveAttendanceType(it.asJsonObject) }
data.setSyncNext(ENDPOINT_VULCAN_API_DICTIONARIES, 4*DAY)
data.setSyncNext(ENDPOINT_VULCAN_API_DICTIONARIES, 4 * DAY)
onSuccess()
}
}
@ -73,7 +73,7 @@ class VulcanApiDictionaries(override val data: DataVulcan, val onSuccess: () ->
}
private fun saveLessonRange(lessonRange: JsonObject) {
val lessonNumber = lessonRange.getInt("Id") ?: return
val lessonNumber = lessonRange.getInt("Numer") ?: return
val startTime = lessonRange.getString("PoczatekTekst")?.let { Time.fromH_m(it) } ?: return
val endTime = lessonRange.getString("KoniecTekst")?.let { Time.fromH_m(it) } ?: return
@ -126,8 +126,7 @@ class VulcanApiDictionaries(override val data: DataVulcan, val onSuccess: () ->
Attendance.TYPE_ABSENT_EXCUSED
else
Attendance.TYPE_ABSENT
}
else {
} else {
val belated = attendanceType.getBoolean("Spoznienie") ?: false
val released = attendanceType.getBoolean("Zwolnienie") ?: false
val present = attendanceType.getBoolean("Obecnosc") ?: true

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-11-12
*/
package pl.szczodrzynski.edziennik.api.v2.vulcan.data.api
import pl.szczodrzynski.edziennik.api.v2.VULCAN_API_ENDPOINT_MESSAGES_CHANGE_STATUS
import pl.szczodrzynski.edziennik.api.v2.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.api.v2.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
class VulcanApiMessagesChangeStatus(
override val data: DataVulcan,
private val messageObject: MessageFull,
val onSuccess: () -> Unit
) : VulcanApi(data) {
companion object {
const val TAG = "VulcanApiMessagesChangeStatus"
}
init {
data.profile?.also { profile ->
apiGet(TAG, VULCAN_API_ENDPOINT_MESSAGES_CHANGE_STATUS, parameters = mapOf(
"WiadomoscId" to messageObject.id,
"FolderWiadomosci" to "Odebrane",
"Status" to "Widoczna",
"LoginId" to data.studentLoginId,
"IdUczen" to data.studentId
)) { _, _ ->
if (!messageObject.seen) {
data.messageMetadataList.add(Metadata(
profileId,
Metadata.TYPE_MESSAGE,
messageObject.id,
true,
true,
messageObject.addedDate
))
}
if (messageObject.type != TYPE_SENT) {
val messageRecipientObject = MessageRecipient(
profileId,
-1,
-1,
System.currentTimeMillis(),
messageObject.id
)
data.messageRecipientList.add(messageRecipientObject)
}
onSuccess()
}
}
}
}

View File

@ -14,6 +14,8 @@ import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
class VulcanApiMessagesInbox(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) {
@ -43,7 +45,22 @@ class VulcanApiMessagesInbox(override val data: DataVulcan, val onSuccess: () ->
val senderLoginId = message.getString("NadawcaId") ?: return@forEach
val senderId = data.teacherList
.singleOrNull { it.loginId == senderLoginId }?.id ?: return@forEach
.singleOrNull { it.loginId == senderLoginId }?.id ?: {
val senderName = message.getString("Nadawca") ?: ""
senderName.getLastFirstName()?.let { (senderLastName, senderFirstName) ->
val teacherObject = Teacher(
profileId,
-1 * Utils.crc16(senderName.toByteArray()).toLong(),
senderFirstName,
senderLastName,
senderLoginId
)
data.teacherList.put(teacherObject.id, teacherObject)
teacherObject.id
}
}.invoke() ?: -1
val sentDate = message.getLong("DataWyslaniaUnixEpoch")?.let { it * 1000 }
?: -1

View File

@ -14,6 +14,8 @@ import pl.szczodrzynski.edziennik.data.db.modules.messages.Message
import pl.szczodrzynski.edziennik.data.db.modules.messages.Message.TYPE_SENT
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
class VulcanApiMessagesSent(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) {
@ -23,11 +25,11 @@ class VulcanApiMessagesSent(override val data: DataVulcan, val onSuccess: () ->
init {
data.profile?.also { profile ->
val startDate: String = when (profile.empty) {
true -> profile.getSemesterStart(profile.currentSemester).stringY_m_d
else -> Date.getToday().stepForward(0, -1, 0).stringY_m_d
val startDate: Long = when (profile.empty) {
true -> profile.getSemesterStart(profile.currentSemester).inUnix
else -> Date.getToday().stepForward(0, -1, 0).inUnix
}
val endDate: String = profile.getSemesterEnd(profile.currentSemester).stringY_m_d
val endDate: Long = profile.getSemesterEnd(profile.currentSemester).inUnix
apiGet(TAG, VULCAN_API_ENDPOINT_MESSAGES_SENT, parameters = mapOf(
"DataPoczatkowa" to startDate,
@ -43,23 +45,27 @@ class VulcanApiMessagesSent(override val data: DataVulcan, val onSuccess: () ->
val unreadBy = message.getInt("Nieprzeczytane") ?: 0
val sentDate = message.getLong("DataWyslaniaUnixEpoch")?.let { it * 1000 } ?: -1
val messageObject = Message(
profileId,
id,
subject,
body,
TYPE_SENT,
-1,
-1
)
message.getJsonArray("Adresaci")?.asJsonObjectList()
?.forEachIndexed { _, recipient ->
?.onEach { receiver ->
val recipientLoginId = recipient.getString("LoginId")
?: return@forEachIndexed
val recipientId = data.teacherList.singleOrNull { it.loginId == recipientLoginId }?.id
?: return@forEachIndexed
val receiverLoginId = receiver.getString("LoginId")
?: return@onEach
val receiverId = data.teacherList.singleOrNull { it.loginId == receiverLoginId }?.id
?: {
val receiverName = receiver.getString("Nazwa") ?: ""
receiverName.getLastFirstName()?.let { (receiverLastName, receiverFirstName) ->
val teacherObject = Teacher(
profileId,
-1 * Utils.crc16(receiverName.toByteArray()).toLong(),
receiverFirstName,
receiverLastName,
receiverLoginId
)
data.teacherList.put(teacherObject.id, teacherObject)
teacherObject.id
}
}.invoke() ?: -1
val readDate: Long = when (readBy) {
0 -> 0
@ -71,7 +77,7 @@ class VulcanApiMessagesSent(override val data: DataVulcan, val onSuccess: () ->
val messageRecipientObject = MessageRecipient(
profileId,
recipientId,
receiverId,
-1,
readDate,
id
@ -80,6 +86,16 @@ class VulcanApiMessagesSent(override val data: DataVulcan, val onSuccess: () ->
data.messageRecipientList.add(messageRecipientObject)
}
val messageObject = Message(
profileId,
id,
subject,
body,
TYPE_SENT,
-1,
-1
)
data.messageIgnoreList.add(messageObject)
data.metadataList.add(Metadata(
profileId,

View File

@ -0,0 +1,213 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-11-13
*/
package pl.szczodrzynski.edziennik.api.v2.vulcan.data.api
import androidx.core.util.set
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.api.v2.Regexes
import pl.szczodrzynski.edziennik.api.v2.VULCAN_API_ENDPOINT_TIMETABLE
import pl.szczodrzynski.edziennik.api.v2.models.DataRemoveModel
import pl.szczodrzynski.edziennik.api.v2.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.api.v2.vulcan.ENDPOINT_VULCAN_API_TIMETABLE
import pl.szczodrzynski.edziennik.api.v2.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.db.modules.api.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.modules.metadata.Metadata
import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject
import pl.szczodrzynski.edziennik.data.db.modules.teams.Team
import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.utils.Utils.crc16
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Week
class VulcanApiTimetable(override val data: DataVulcan, val onSuccess: () -> Unit) : VulcanApi(data) {
companion object {
const val TAG = "VulcanApiTimetable"
}
init { data.profile?.also { profile ->
val currentWeekStart = Week.getWeekStart()
if (Date.getToday().weekDay > 4) {
currentWeekStart.stepForward(0, 0, 7)
}
val getDate = data.arguments?.getString("weekStart") ?: currentWeekStart.stringY_m_d
val weekStart = Date.fromY_m_d(getDate)
val weekEnd = weekStart.clone().stepForward(0, 0, 6)
apiGet(TAG, VULCAN_API_ENDPOINT_TIMETABLE, parameters = mapOf(
"DataPoczatkowa" to weekStart.stringY_m_d,
"DataKoncowa" to weekEnd.stringY_m_d,
"IdUczen" to data.studentId,
"IdOddzial" to data.studentClassId,
"IdOkresKlasyfikacyjny" to data.studentSemesterId
)) { json, _ ->
val dates = mutableSetOf<Int>()
val lessons = mutableListOf<Lesson>()
json.getJsonArray("Data")?.asJsonObjectList()?.forEach { lesson ->
if (lesson.getBoolean("PlanUcznia") != true)
return@forEach
val lessonDate = Date.fromY_m_d(lesson.getString("DzienTekst"))
val lessonNumber = lesson.getInt("NumerLekcji")
val lessonRange = data.lessonRanges.singleOrNull { it.lessonNumber == lessonNumber }
val startTime = lessonRange?.startTime
val endTime = lessonRange?.endTime
val teacherId = lesson.getLong("IdPracownik")
val classroom = lesson.getString("Sala")
val oldTeacherId = lesson.getLong("IdPracownikOld")
val changeAnnotation = lesson.getString("AdnotacjaOZmianie") ?: ""
val type = when {
changeAnnotation.startsWith("(przeniesiona z") -> Lesson.TYPE_SHIFTED_TARGET
changeAnnotation.startsWith("(przeniesiona na") -> Lesson.TYPE_SHIFTED_SOURCE
changeAnnotation.startsWith("(zastępstwo") -> Lesson.TYPE_CHANGE
lesson.getBoolean("PrzekreslonaNazwa") == true -> Lesson.TYPE_CANCELLED
else -> Lesson.TYPE_NORMAL
}
val teamId = lesson.getString("PodzialSkrot")?.let { teamName ->
val name = "${data.teamClass?.name} $teamName"
val id = name.crc16().toLong()
var team = data.teamList.singleOrNull { it.name == name }
if (team == null) {
team = Team(
profileId,
id,
name,
Team.TYPE_VIRTUAL,
"${data.schoolName}:$name",
teacherId ?: oldTeacherId ?: -1
)
data.teamList[id] = team
}
team.id
} ?: data.studentClassId.toLong()
val subjectId = lesson.getLong("IdPrzedmiot")?.let {
when (it) {
0L -> {
val subjectName = lesson.getString("PrzedmiotNazwa") ?: ""
data.subjectList.singleOrNull { subject -> subject.longName == subjectName }?.id
?: {
/**
* CREATE A NEW SUBJECT IF IT DOESN'T EXIST
*/
val subjectObject = Subject(
profileId,
-1 * crc16(subjectName.toByteArray()).toLong(),
subjectName,
subjectName
)
data.subjectList.put(subjectObject.id, subjectObject)
subjectObject.id
}.invoke()
}
else -> it
}
}
val lessonObject = Lesson(profileId, -1).apply {
this.type = type
when (type) {
Lesson.TYPE_NORMAL, Lesson.TYPE_CHANGE, Lesson.TYPE_SHIFTED_TARGET -> {
this.date = lessonDate
this.lessonNumber = lessonNumber
this.startTime = startTime
this.endTime = endTime
this.subjectId = subjectId
this.teacherId = teacherId
this.teamId = teamId
this.classroom = classroom
this.oldTeacherId = oldTeacherId
}
Lesson.TYPE_CANCELLED, Lesson.TYPE_SHIFTED_SOURCE -> {
this.oldDate = lessonDate
this.oldLessonNumber = lessonNumber
this.oldStartTime = startTime
this.oldEndTime = endTime
this.oldSubjectId = subjectId
this.oldTeacherId = teacherId
this.oldTeamId = teamId
this.oldClassroom = classroom
}
}
if (type == Lesson.TYPE_SHIFTED_SOURCE || type == Lesson.TYPE_SHIFTED_TARGET) {
val shift = Regexes.VULCAN_SHITFT_ANNOTATION.find(changeAnnotation)
val oldLessonNumber = shift?.get(2)?.toInt()
val oldLessonDate = shift?.get(3)?.let { Date.fromd_m_Y(it) }
val oldLessonRange = data.lessonRanges.singleOrNull { it.lessonNumber == oldLessonNumber }
val oldStartTime = oldLessonRange?.startTime
val oldEndTime = oldLessonRange?.endTime
when (type) {
Lesson.TYPE_SHIFTED_SOURCE -> {
this.lessonNumber = oldLessonNumber
this.date = oldLessonDate
this.startTime = oldStartTime
this.endTime = oldEndTime
}
Lesson.TYPE_SHIFTED_TARGET -> {
this.oldLessonNumber = oldLessonNumber
this.oldDate = oldLessonDate
this.oldStartTime = oldStartTime
this.oldEndTime = oldEndTime
}
}
}
this.id = buildId()
}
val seen = profile.empty || lessonDate < Date.getToday()
if (type != Lesson.TYPE_NORMAL) {
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_LESSON_CHANGE,
lessonObject.id,
seen,
seen,
System.currentTimeMillis()
))
}
dates.add(lessonDate.value)
lessons.add(lessonObject)
}
val date: Date = weekStart.clone()
while (date <= weekEnd) {
if (!dates.contains(date.value)) {
lessons.add(Lesson(profileId, date.value.toLong()).apply {
this.type = Lesson.TYPE_NO_LESSONS
this.date = date.clone()
})
}
date.stepForward(0, 0, 1)
}
d(TAG, "Clearing lessons between ${weekStart.stringY_m_d} and ${weekEnd.stringY_m_d} - timetable downloaded for $getDate")
data.lessonNewList.addAll(lessons)
data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd))
data.setSyncNext(ENDPOINT_VULCAN_API_TIMETABLE, SYNC_ALWAYS)
onSuccess()
}
}}
}

View File

@ -38,10 +38,12 @@ class VulcanLoginApi(val data: DataVulcan, val onSuccess: () -> Unit) {
if (data.apiToken?.get(0) == 'F') VULCAN_API_PASSWORD_FAKELOG else VULCAN_API_PASSWORD,
data.apiCertificatePfx ?: ""
)
onSuccess()
return@run
data.loginStore.removeLoginData("certificatePfx")
} catch (e: Throwable) {
e.printStackTrace()
} finally {
onSuccess()
return@run
}
}
if (data.symbol.isNotNullNorEmpty() && data.apiToken.isNotNullNorEmpty() && data.apiPin.isNotNullNorEmpty()) {

View File

@ -1,11 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.interfaces;
import im.wangchao.mhttp.Request;
/**
* Callback containing a {@link Request.Builder} which has correct headers and body to download a corresponding message attachment when ran.
* {@code onSuccess} has to be ran on the UI thread.
*/
public interface AttachmentGetCallback {
void onSuccess(Request.Builder builder);
}

View File

@ -1,92 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.interfaces;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Map;
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore;
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull;
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile;
import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull;
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher;
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeInfo;
import pl.szczodrzynski.edziennik.utils.models.Endpoint;
public interface EdziennikInterface {
/**
* Sync all Edziennik data.
* Ran always on worker thread.
*
* @param activityContext a {@link Context}, used for resource extractions, passed back to {@link SyncCallback}
* @param callback ran on worker thread.
* @param profileId
* @param profile
* @param loginStore
*/
void sync(@NonNull Context activityContext, @NonNull SyncCallback callback, int profileId, @Nullable Profile profile, @NonNull LoginStore loginStore);
void syncMessages(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile);
void syncFeature(@NonNull Context activityContext, @NonNull SyncCallback callback, @NonNull ProfileFull profile, int ... featureList);
int FEATURE_ALL = 0;
int FEATURE_TIMETABLE = 1;
int FEATURE_AGENDA = 2;
int FEATURE_GRADES = 3;
int FEATURE_HOMEWORK = 4;
int FEATURE_NOTICES = 5;
int FEATURE_ATTENDANCE = 6;
int FEATURE_MESSAGES_INBOX = 7;
int FEATURE_MESSAGES_OUTBOX = 8;
int FEATURE_ANNOUNCEMENTS = 9;
/**
* Download a single message or get its recipient list if it's already downloaded.
*
* May be executed on any thread.
*
* @param activityContext
* @param errorCallback used for error reporting. Ran on a background thread.
* @param profile
* @param message a message of which body and recipient list should be downloaded.
* @param messageCallback always executed on UI thread.
*/
void getMessage(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull MessageFull message, @NonNull MessageGetCallback messageCallback);
void getAttachment(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull MessageFull message, long attachmentId, @NonNull AttachmentGetCallback attachmentCallback);
//void getMessageList(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, int type, @NonNull MessageListCallback messageCallback);
/**
* Download a list of available message recipients.
*
* Updates a database-saved {@code teacherList} with {@code loginId}s.
*
* A {@link Teacher} is considered as a recipient when its {@code loginId} is not null.
*
* May be executed on any thread.
*
* @param activityContext
* @param errorCallback used for error reporting. Ran on a background thread.
* @param profile
* @param recipientListGetCallback always executed on UI thread.
*/
void getRecipientList(@NonNull Context activityContext, @NonNull SyncCallback errorCallback, @NonNull ProfileFull profile, @NonNull RecipientListGetCallback recipientListGetCallback);
MessagesComposeInfo getComposeInfo(@NonNull ProfileFull profile);
/**
*
* @param profile a {@link Profile} containing already changed endpoints
* @return a map of configurable {@link Endpoint}s along with their names, {@code null} when unsupported
*/
Map<String, Endpoint> getConfigurableEndpoints(Profile profile);
/**
* Check if the specified endpoint is enabled for the current profile.
*
* @param profile a {@link Profile} containing already changed endpoints
* @param defaultActive if the endpoint is enabled by default.
* @param name the endpoint's name
* @return {@code true} if the endpoint is enabled, {@code false} when it's not. Return {@code defaultActive} if unsupported.
*/
boolean isEndpointEnabled(Profile profile, boolean defaultActive, String name);
}

View File

@ -1,11 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.interfaces;
import android.content.Context;
import androidx.annotation.NonNull;
import pl.szczodrzynski.edziennik.data.api.AppError;
public interface ErrorCallback {
void onError(Context activityContext, @NonNull AppError error);
}

View File

@ -1,5 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.interfaces;
public interface LoginCallback {
void onSuccess();
}

View File

@ -1,11 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.interfaces;
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull;
/**
* Callback containing a {@link MessageFull} which already has its {@code body} and {@code recipients}.
* {@code onSuccess} is always ran on the UI thread.
*/
public interface MessageGetCallback {
void onSuccess(MessageFull message);
}

View File

@ -1,9 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.interfaces;
import java.util.List;
import pl.szczodrzynski.edziennik.data.db.modules.messages.MessageFull;
public interface MessageListCallback {
void onSuccess(List<MessageFull> messageList);
}

View File

@ -1,8 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.interfaces;
import androidx.annotation.StringRes;
public interface ProgressCallback extends ErrorCallback {
void onProgress(int progressStep);
void onActionStarted(@StringRes int stringResId);
}

View File

@ -1,9 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.interfaces;
import java.util.List;
import pl.szczodrzynski.edziennik.data.db.modules.teachers.Teacher;
public interface RecipientListGetCallback {
void onSuccess(List<Teacher> teacherList);
}

View File

@ -1,19 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.interfaces;
import android.content.Context;
import java.util.List;
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore;
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile;
import pl.szczodrzynski.edziennik.data.db.modules.profiles.ProfileFull;
/**
* A callback used for error reporting, progress information.
* All the methods are always ran on a worker thread.
*/
public interface SyncCallback extends ProgressCallback {
void onLoginFirst(List<Profile> profileList, LoginStore loginStore);
void onSuccess(Context activityContext, ProfileFull profileFull);
}

View File

@ -1,6 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.v2
import pl.szczodrzynski.edziennik.data.api.AppError
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
data class ApiLoginResult(val loginStore: LoginStore, val error: AppError?)

View File

@ -1,6 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.v2
import pl.szczodrzynski.edziennik.data.api.AppError
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
data class FirstLoginResult(val profileList: ArrayList<Profile>, val error: AppError?)

View File

@ -1,12 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.v2.librus.firstlogin
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.interfaces.ProgressCallback
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
class FirstLoginLibrus(val app: App, val loginStore: LoginStore, val progressCallback: ProgressCallback, val onSuccess: (profileList: List<Profile>) -> Unit) {
init {
}
}

View File

@ -1,12 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.v2.librus.firstlogin
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.interfaces.ProgressCallback
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
class FirstLoginSynergia(val app: App, val loginStore: LoginStore, val progressCallback: ProgressCallback, val onSuccess: (profileList: List<Profile>) -> Unit) {
init {
}
}

View File

@ -1,10 +1,10 @@
package pl.szczodrzynski.edziennik.data.db.modules.grades;
import androidx.room.Entity;
import java.util.ArrayList;
import java.util.List;
import androidx.room.Entity;
@Entity(tableName = "gradeCategories",
primaryKeys = {"profileId", "categoryId"})
public class GradeCategory {
@ -25,6 +25,9 @@ public class GradeCategory {
*/
public int type = 0;
public static final int TYPE_NORMAL = 0;
public static final int TYPE_COMMENT = 1;
public GradeCategory(int profileId, long categoryId, float weight, int color, String text) {
this.profileId = profileId;
this.categoryId = categoryId;

View File

@ -37,7 +37,10 @@ public abstract class LuckyNumberDao {
@Nullable
@Query("SELECT * FROM luckyNumbers WHERE profileId = :profileId AND luckyNumberDate >= :date ORDER BY luckyNumberDate DESC LIMIT 1")
public abstract LuckyNumber getNearestFutureNow(int profileId, Date date);
public abstract LuckyNumber getNearestFutureNow(int profileId, int date);
@Query("SELECT * FROM luckyNumbers WHERE profileId = :profileId AND luckyNumberDate >= :date ORDER BY luckyNumberDate DESC LIMIT 1")
public abstract LiveData<LuckyNumber> getNearestFuture(int profileId, int date);
@RawQuery(observedEntities = {LuckyNumber.class})
abstract LiveData<List<LuckyNumberFull>> getAll(SupportSQLiteQuery query);

View File

@ -21,7 +21,10 @@ interface NotificationDao {
@Query("DELETE FROM notifications WHERE profileId = :profileId")
fun clear(profileId: Int)
@Query("SELECT * FROM notifications")
@Query("DELETE FROM notifications")
fun clearAll()
@Query("SELECT * FROM notifications ORDER BY addedDate DESC")
fun getAll(): LiveData<List<Notification>>
@Query("SELECT * FROM notifications")

View File

@ -15,7 +15,7 @@ import pl.szczodrzynski.edziennik.utils.models.Time
Index(value = ["profileId", "type", "date"]),
Index(value = ["profileId", "type", "oldDate"])
])
open class Lesson(val profileId: Int, @PrimaryKey val id: Long) {
open class Lesson(val profileId: Int, @PrimaryKey var id: Long) {
companion object {
const val TYPE_NO_LESSONS = -1
const val TYPE_NORMAL = 0
@ -45,6 +45,22 @@ open class Lesson(val profileId: Int, @PrimaryKey val id: Long) {
var oldTeamId: Long? = null
var oldClassroom: String? = null
val displayDate: Date?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldDate
return date ?: oldDate
}
val displayStartTime: Time?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldStartTime
return startTime ?: oldStartTime
}
fun buildId(): Long = (displayDate?.combineWith(displayStartTime) ?: 0L) / 6L * 10L + (hashCode() and 0xFFFF)
override fun toString(): String {
return "Lesson(profileId=$profileId, " +
"id=$id, " +
@ -66,6 +82,55 @@ open class Lesson(val profileId: Int, @PrimaryKey val id: Long) {
"oldTeamId=$oldTeamId, " +
"oldClassroom=$oldClassroom)"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Lesson) return false
if (profileId != other.profileId) return false
if (id != other.id) return false
if (type != other.type) return false
if (date != other.date) return false
if (lessonNumber != other.lessonNumber) return false
if (startTime != other.startTime) return false
if (endTime != other.endTime) return false
if (subjectId != other.subjectId) return false
if (teacherId != other.teacherId) return false
if (teamId != other.teamId) return false
if (classroom != other.classroom) return false
if (oldDate != other.oldDate) return false
if (oldLessonNumber != other.oldLessonNumber) return false
if (oldStartTime != other.oldStartTime) return false
if (oldEndTime != other.oldEndTime) return false
if (oldSubjectId != other.oldSubjectId) return false
if (oldTeacherId != other.oldTeacherId) return false
if (oldTeamId != other.oldTeamId) return false
if (oldClassroom != other.oldClassroom) return false
return true
}
override fun hashCode(): Int {
var result = profileId
result = 31 * result + type
result = 31 * result + (date?.hashCode() ?: 0)
result = 31 * result + (lessonNumber ?: 0)
result = 31 * result + (startTime?.hashCode() ?: 0)
result = 31 * result + (endTime?.hashCode() ?: 0)
result = 31 * result + (subjectId?.hashCode() ?: 0)
result = 31 * result + (teacherId?.hashCode() ?: 0)
result = 31 * result + (teamId?.hashCode() ?: 0)
result = 31 * result + (classroom?.hashCode() ?: 0)
result = 31 * result + (oldDate?.hashCode() ?: 0)
result = 31 * result + (oldLessonNumber ?: 0)
result = 31 * result + (oldStartTime?.hashCode() ?: 0)
result = 31 * result + (oldEndTime?.hashCode() ?: 0)
result = 31 * result + (oldSubjectId?.hashCode() ?: 0)
result = 31 * result + (oldTeacherId?.hashCode() ?: 0)
result = 31 * result + (oldTeamId?.hashCode() ?: 0)
result = 31 * result + (oldClassroom?.hashCode() ?: 0)
return result
}
}
/*
DROP TABLE lessons;
@ -94,4 +159,4 @@ CREATE TABLE lessons (
PRIMARY KEY(profileId)
);
*/
*/

View File

@ -1,6 +1,7 @@
package pl.szczodrzynski.edziennik.data.db.modules.timetable
import pl.szczodrzynski.edziennik.utils.models.Date
import android.content.Context
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.utils.models.Time
class LessonFull(profileId: Int, id: Long) : Lesson(profileId, id) {
@ -11,24 +12,13 @@ class LessonFull(profileId: Int, id: Long) : Lesson(profileId, id) {
var oldTeacherName: String? = null
var oldTeamName: String? = null
val displayDate: Date?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldDate
return date ?: oldDate
}
val displayLessonNumber: Int?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldLessonNumber
return lessonNumber ?: oldLessonNumber
}
val displayStartTime: Time?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldStartTime
return startTime ?: oldStartTime
}
val displayEndTime: Time?
get() {
if (type == TYPE_SHIFTED_SOURCE)
@ -42,12 +32,14 @@ class LessonFull(profileId: Int, id: Long) : Lesson(profileId, id) {
return oldSubjectName
return subjectName ?: oldSubjectName
}
val displayTeacherName: String?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldTeacherName
return teacherName ?: oldTeacherName
}
val displayTeamName: String?
get() {
if (type == TYPE_SHIFTED_SOURCE)
@ -68,12 +60,14 @@ class LessonFull(profileId: Int, id: Long) : Lesson(profileId, id) {
return oldTeamId
return teamId ?: oldTeamId
}
val displaySubjectId: Long?
get() {
if (type == TYPE_SHIFTED_SOURCE)
return oldSubjectId
return subjectId ?: oldSubjectId
}
val displayTeacherId: Long?
get() {
if (type == TYPE_SHIFTED_SOURCE)
@ -81,8 +75,35 @@ class LessonFull(profileId: Int, id: Long) : Lesson(profileId, id) {
return teacherId ?: oldTeacherId
}
fun getDisplayChangeType(context: Context): String {
return context.getString(when (type) {
TYPE_CHANGE -> R.string.lesson_change
TYPE_CANCELLED -> R.string.lesson_cancelled
TYPE_SHIFTED_TARGET, TYPE_SHIFTED_SOURCE -> R.string.lesson_shifted
else -> R.string.lesson_timetable_change
})
}
val changeSubjectName: String
get() {
val first = when (type) {
TYPE_CHANGE, TYPE_CANCELLED, TYPE_SHIFTED_SOURCE -> oldSubjectName
else -> subjectName
}
val second = when (type) {
TYPE_CHANGE -> subjectName
else -> null
}
return when (second) {
null -> first ?: ""
else -> "$first -> $second"
}
}
// metadata
var seen: Boolean = false
var notified: Boolean = false
var addedDate: Long = 0
}
}

View File

@ -10,6 +10,27 @@ import pl.szczodrzynski.edziennik.utils.models.Date
@Dao
interface TimetableDao {
companion object {
private const val QUERY = """
SELECT
timetable.*,
subjects.subjectLongName AS subjectName,
teachers.teacherName ||" "|| teachers.teacherSurname AS teacherName,
teams.teamName AS teamName,
oldS.subjectLongName AS oldSubjectName,
oldT.teacherName ||" "|| oldT.teacherSurname AS oldTeacherName,
oldG.teamName AS oldTeamName,
metadata.seen, metadata.notified, metadata.addedDate
FROM timetable
LEFT JOIN subjects USING(profileId, subjectId)
LEFT JOIN teachers USING(profileId, teacherId)
LEFT JOIN teams USING(profileId, teamId)
LEFT JOIN subjects AS oldS ON timetable.profileId = oldS.profileId AND timetable.oldSubjectId = oldS.subjectId
LEFT JOIN teachers AS oldT ON timetable.profileId = oldT.profileId AND timetable.oldTeacherId = oldT.teacherId
LEFT JOIN teams AS oldG ON timetable.profileId = oldG.profileId AND timetable.oldTeamId = oldG.teamId
LEFT JOIN metadata ON id = thingId AND thingType = ${Metadata.TYPE_LESSON_CHANGE} AND metadata.profileId = timetable.profileId
"""
}
@Insert(onConflict = OnConflictStrategy.REPLACE)
operator fun plusAssign(lessonList: List<Lesson>)
@ -17,57 +38,60 @@ interface TimetableDao {
@Query("DELETE FROM timetable WHERE profileId = :profileId")
fun clear(profileId: Int)
@Query("DELETE FROM timetable WHERE profileId = :profileId AND (type != 3 AND date >= :dateFrom) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom)")
@Query("DELETE FROM timetable WHERE profileId = :profileId AND ((type != 3 AND date >= :dateFrom) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom))")
fun clearFromDate(profileId: Int, dateFrom: Date)
@Query("DELETE FROM timetable WHERE profileId = :profileId AND (type != 3 AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate <= :dateTo)")
@Query("DELETE FROM timetable WHERE profileId = :profileId AND ((type != 3 AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate <= :dateTo))")
fun clearToDate(profileId: Int, dateTo: Date)
@Query("DELETE FROM timetable WHERE profileId = :profileId AND (type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo)")
@Query("DELETE FROM timetable WHERE profileId = :profileId AND ((type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo))")
fun clearBetweenDates(profileId: Int, dateFrom: Date, dateTo: Date)
@Query("""
SELECT
timetable.*,
subjects.subjectLongName AS subjectName,
teachers.teacherName ||" "|| teachers.teacherSurname AS teacherName,
teams.teamName AS teamName,
oldS.subjectLongName AS oldSubjectName,
oldT.teacherName ||" "|| oldT.teacherSurname AS oldTeacherName,
oldG.teamName AS oldTeamName,
metadata.seen, metadata.notified, metadata.addedDate
FROM timetable
LEFT JOIN subjects USING(profileId, subjectId)
LEFT JOIN teachers USING(profileId, teacherId)
LEFT JOIN teams USING(profileId, teamId)
LEFT JOIN subjects AS oldS ON timetable.profileId = oldS.profileId AND timetable.oldSubjectId = oldS.subjectId
LEFT JOIN teachers AS oldT ON timetable.profileId = oldT.profileId AND timetable.oldTeacherId = oldT.teacherId
LEFT JOIN teams AS oldG ON timetable.profileId = oldG.profileId AND timetable.oldTeamId = oldG.teamId
LEFT JOIN metadata ON id = thingId AND thingType = ${Metadata.TYPE_LESSON_CHANGE} AND metadata.profileId = timetable.profileId
WHERE timetable.profileId = :profileId AND (type != 3 AND date = :date) OR ((type = 3 OR type = 1) AND oldDate = :date)
$QUERY
WHERE timetable.profileId = :profileId AND ((type != 3 AND date = :date) OR ((type = 3 OR type = 1) AND oldDate = :date))
ORDER BY id, type
""")
fun getForDate(profileId: Int, date: Date) : LiveData<List<LessonFull>>
@Query("""
SELECT
timetable.*,
subjects.subjectLongName AS subjectName,
teachers.teacherName ||" "|| teachers.teacherSurname AS teacherName,
teams.teamName AS teamName,
oldS.subjectLongName AS oldSubjectName,
oldT.teacherName ||" "|| oldT.teacherSurname AS oldTeacherName,
oldG.teamName AS oldTeamName,
metadata.seen, metadata.notified, metadata.addedDate
FROM timetable
LEFT JOIN subjects USING(profileId, subjectId)
LEFT JOIN teachers USING(profileId, teacherId)
LEFT JOIN teams USING(profileId, teamId)
LEFT JOIN subjects AS oldS ON timetable.profileId = oldS.profileId AND timetable.oldSubjectId = oldS.subjectId
LEFT JOIN teachers AS oldT ON timetable.profileId = oldT.profileId AND timetable.oldTeacherId = oldT.teacherId
LEFT JOIN teams AS oldG ON timetable.profileId = oldG.profileId AND timetable.oldTeamId = oldG.teamId
LEFT JOIN metadata ON id = thingId AND thingType = ${Metadata.TYPE_LESSON_CHANGE} AND metadata.profileId = timetable.profileId
$QUERY
WHERE timetable.profileId = :profileId AND ((type != 3 AND date > :today) OR ((type = 3 OR type = 1) AND oldDate > :today)) AND timetable.subjectId = :subjectId
ORDER BY id, type
LIMIT 1
""")
fun getNextWithSubject(profileId: Int, today: Date, subjectId: Long) : LiveData<LessonFull>
fun getNextWithSubject(profileId: Int, today: Date, subjectId: Long) : LiveData<LessonFull?>
@Query("""
$QUERY
WHERE timetable.profileId = :profileId AND ((type != 3 AND date > :today) OR ((type = 3 OR type = 1) AND oldDate > :today)) AND timetable.subjectId = :subjectId AND timetable.teamId = :teamId
ORDER BY id, type
LIMIT 1
""")
fun getNextWithSubjectAndTeam(profileId: Int, today: Date, subjectId: Long, teamId: Long): LiveData<LessonFull?>
@Query("""
$QUERY
WHERE (type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo)
ORDER BY profileId, id, type
""")
fun getBetweenDatesNow(dateFrom: Date, dateTo: Date) : List<LessonFull>
@Query("""
$QUERY
WHERE (type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo)
ORDER BY profileId, id, type
""")
fun getBetweenDates(dateFrom: Date, dateTo: Date) : LiveData<List<LessonFull>>
@Query("""
$QUERY
WHERE timetable.profileId = :profileId AND timetable.id = :lessonId
ORDER BY id, type
""")
fun getByIdNow(profileId: Int, lessonId: Long) : LessonFull?
@Query("""
$QUERY
WHERE timetable.profileId = :profileId AND timetable.type NOT IN (${Lesson.TYPE_NORMAL}, ${Lesson.TYPE_NO_LESSONS}, ${Lesson.TYPE_SHIFTED_SOURCE}) AND metadata.notified = 0
""")
fun getNotNotifiedNow(profileId: Int): List<LessonFull>
}

View File

@ -125,19 +125,22 @@ public class BootReceiver extends BroadcastReceiver {
String updateUrl = result.get("update_url").getAsString();
String updateFilename = result.get("update_filename").getAsString();
boolean updateMandatory = result.get("update_mandatory").getAsBoolean();
boolean updateDirect = result.get("update_direct").getAsBoolean();
if (app.appConfig.updateVersion == null || !app.appConfig.updateVersion.equals(updateVersion)) {
app.appConfig.updateVersion = updateVersion;
app.appConfig.updateUrl = updateUrl;
app.appConfig.updateFilename = updateFilename;
app.appConfig.updateMandatory = updateMandatory;
app.appConfig.updateDirect = updateDirect;
app.saveConfig();
}
if (!UPDATES_ON_PLAY_STORE || intent.getBooleanExtra("UserChecked", false)) {
app.notifier.notificationUpdatesShow(
updateVersion,
updateUrl,
updateFilename);
updateFilename,
updateDirect);
}
} else {
if (app.appConfig.updateVersion == null || !app.appConfig.updateVersion.equals("")) {
@ -259,7 +262,7 @@ public class BootReceiver extends BroadcastReceiver {
@Override
protected void onHandleIntent(Intent intent) {
if (UPDATES_ON_PLAY_STORE) {
if (UPDATES_ON_PLAY_STORE && !intent.getBooleanExtra("update_direct", false)) {
Utils.openGooglePlay(this, "pl.szczodrzynski.edziennik");
return;
}
@ -270,7 +273,8 @@ public class BootReceiver extends BroadcastReceiver {
app.notifier.notificationUpdatesShow(
intent.getStringExtra("update_version"),
intent.getStringExtra("update_url"),
intent.getStringExtra("update_filename"));
intent.getStringExtra("update_filename"),
intent.getBooleanExtra("update_direct", false));
return;
}
}

View File

@ -154,19 +154,22 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
String updateUrl = remoteMessage.getData().get("update_url");
String updateFilename = remoteMessage.getData().get("update_filename");
boolean updateMandatory = Boolean.parseBoolean(remoteMessage.getData().get("update_mandatory"));
boolean updateDirect = Boolean.parseBoolean(remoteMessage.getData().get("update_direct"));
if (app.appConfig.updateVersion == null || !app.appConfig.updateVersion.equals(updateVersion)) {
app.appConfig.updateVersion = updateVersion;
app.appConfig.updateUrl = updateUrl;
app.appConfig.updateFilename = updateFilename;
app.appConfig.updateMandatory = updateMandatory;
app.appConfig.updateDirect = updateDirect;
app.saveConfig("updateVersion", "updateUrl", "updateFilename", "updateMandatory");
}
if (!remoteMessage.getData().containsKey("update_silent")) {
app.notifier.notificationUpdatesShow(
updateVersion,
updateUrl,
updateFilename);
updateFilename,
updateDirect);
}
} else {
if (app.appConfig.updateVersion == null || !app.appConfig.updateVersion.equals("")) {

View File

@ -36,7 +36,6 @@ import java.util.List;
import pl.szczodrzynski.edziennik.App;
import pl.szczodrzynski.edziennik.MainActivity;
import pl.szczodrzynski.edziennik.R;
import pl.szczodrzynski.edziennik.data.api.AppError;
import pl.szczodrzynski.edziennik.data.db.modules.events.Event;
import pl.szczodrzynski.edziennik.data.db.modules.events.EventFull;
import pl.szczodrzynski.edziennik.data.db.modules.events.EventType;
@ -54,7 +53,6 @@ import pl.szczodrzynski.edziennik.utils.models.Time;
import pl.szczodrzynski.edziennik.utils.models.Week;
import static pl.szczodrzynski.edziennik.App.APP_URL;
import static pl.szczodrzynski.edziennik.data.api.AppError.CODE_OTHER;
import static pl.szczodrzynski.edziennik.data.db.modules.events.Event.COLOR_DEFAULT;
import static pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_HOMEWORK;
import static pl.szczodrzynski.edziennik.data.db.modules.events.Event.TYPE_UNDEFINED;
@ -374,7 +372,8 @@ public class EventManualDialog {
}
} catch (Exception e) {
activity.runOnUiThread(() -> {
app.apiEdziennik.guiShowErrorDialog(activity, new AppError(TAG, 379, CODE_OTHER, null, e), R.string.error_occured);
// TODO show error in EventManualDialog
//app.apiEdziennik.guiShowErrorDialog(activity, new AppError(TAG, 379, CODE_OTHER, null, e), R.string.error_occured);
});
}
});
@ -518,7 +517,7 @@ public class EventManualDialog {
registerEventManualDateLayout = dialogView.findViewById(R.id.registerEventManualDateLayout);
registerEventManualDate = dialogView.findViewById(R.id.registerEventManualDate);
registerEventManualDate.setCompoundDrawablesWithIntrinsicBounds(null, null, new IconicsDrawable(context, CommunityMaterial.Icon.cmd_calendar).size(IconicsSize.dp(16)).color(IconicsColor.colorInt(primaryTextColor)), null);
registerEventManualDate.setCompoundDrawablesWithIntrinsicBounds(null, null, new IconicsDrawable(context, CommunityMaterial.Icon.cmd_calendar_outline).size(IconicsSize.dp(16)).color(IconicsColor.colorInt(primaryTextColor)), null);
//registerEventManualDate.setCompoundDrawablePadding(Utils.dpToPx(6));
registerEventManualLessonLayout = dialogView.findViewById(R.id.registerEventManualLessonLayout);
registerEventManualLesson = dialogView.findViewById(R.id.registerEventManualLesson);

View File

@ -4,19 +4,26 @@
package pl.szczodrzynski.edziennik.ui.dialogs.event
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.jaredrummler.android.colorpicker.ColorPickerDialog
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.modules.events.Event
import pl.szczodrzynski.edziennik.data.db.modules.events.EventType
import pl.szczodrzynski.edziennik.data.db.modules.subjects.Subject
import pl.szczodrzynski.edziennik.data.db.modules.teams.Team
import pl.szczodrzynski.edziennik.data.db.modules.timetable.Lesson
import pl.szczodrzynski.edziennik.data.db.modules.timetable.LessonFull
import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding
import pl.szczodrzynski.edziennik.utils.Anim
import pl.szczodrzynski.edziennik.utils.TextInputDropDown
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
@ -30,7 +37,9 @@ class EventManualV2Dialog(
val defaultDate: Date? = null,
val defaultTime: Time? = null,
val defaultType: Int? = null,
val editingEvent: Event? = null
val editingEvent: Event? = null,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
@ -44,18 +53,25 @@ class EventManualV2Dialog(
private val app by lazy { activity.application as App }
private lateinit var b: DialogEventManualV2Binding
private lateinit var dialog: AlertDialog
private lateinit var event: Event
private var defaultLoaded = false
init { run {
job = Job()
private lateinit var event: Event
private var customColor: Int? = null
init { run {
if (activity.isFinishing)
return@run
job = Job()
onShowListener?.invoke(TAG)
b = DialogEventManualV2Binding.inflate(activity.layoutInflater)
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.dialog_event_manual_title)
.setView(b.root)
.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
.setPositiveButton(R.string.save) { _, _ -> saveEvent() }
.setOnDismissListener {
onDismissListener?.invoke(TAG)
}
.show()
event = editingEvent?.clone() ?: Event().also { event ->
@ -71,11 +87,46 @@ class EventManualV2Dialog(
defaultType?.let {
event.type = it
}*/
b.shareSwitch.isChecked = event.sharedBy != null
}
b.showMore.onClick { // TODO iconics is broken
it.apply {
refreshDrawableState()
if (isChecked)
Anim.expand(b.moreLayout, 200, null)
else
Anim.collapse(b.moreLayout, 200, null)
}
}
updateShareText()
b.shareSwitch.onChange { _, isChecked ->
updateShareText(isChecked)
}
loadLists()
}}
private fun updateShareText(checked: Boolean = b.shareSwitch.isChecked) {
val editingShared = editingEvent?.sharedBy != null
val editingOwn = editingEvent?.sharedBy == "self"
b.shareDetails.visibility = if (checked || editingShared)
View.VISIBLE
else View.GONE
val text = when {
checked && editingShared && editingOwn -> R.string.dialog_event_manual_share_will_change
checked && editingShared -> R.string.dialog_event_manual_share_will_request
!checked && editingShared -> R.string.dialog_event_manual_share_will_remove
else -> R.string.dialog_event_manual_share_first_notice
}
b.shareDetails.setText(text)
}
private fun loadLists() { launch {
val deferred = async(Dispatchers.Default) {
// get the team list
@ -107,18 +158,33 @@ class EventManualV2Dialog(
""
)
b.teacherDropdown += teachers.map { TextInputDropDown.Item(it.id, it.fullName, tag = it) }
// get the event type list
val eventTypes = app.db.eventTypeDao().getAllNow(profileId)
b.typeDropdown.clear()
b.typeDropdown += eventTypes.map { TextInputDropDown.Item(it.id, it.name, tag = it) }
}
deferred.await()
b.teamDropdown.isEnabled = true
b.subjectDropdown.isEnabled = true
b.teacherDropdown.isEnabled = true
b.typeDropdown.isEnabled = true
b.typeDropdown.selected?.let { item ->
customColor = (item.tag as EventType).color
}
// copy IDs from event being edited
editingEvent?.let {
b.teamDropdown.select(it.teamId)
b.subjectDropdown.select(it.subjectId)
b.teacherDropdown.select(it.teacherId)
b.typeDropdown.select(it.type)?.let { item ->
customColor = (item.tag as EventType).color
}
if (it.color != -1)
customColor = it.color
}
// copy IDs from the LessonFull
@ -128,6 +194,30 @@ class EventManualV2Dialog(
b.teacherDropdown.select(it.displayTeacherId)
}
b.typeDropdown.setOnChangeListener {
b.typeColor.background.colorFilter = PorterDuffColorFilter((it.tag as EventType).color, PorterDuff.Mode.SRC_ATOP)
customColor = null
return@setOnChangeListener true
}
(customColor ?: Event.COLOR_DEFAULT).let {
b.typeColor.background.colorFilter = PorterDuffColorFilter(it, PorterDuff.Mode.SRC_ATOP)
}
b.typeColor.onClick {
val currentColor = (b.typeDropdown?.selected?.tag as EventType?)?.color ?: Event.COLOR_DEFAULT
val colorPickerDialog = ColorPickerDialog.newBuilder()
.setColor(currentColor)
.create()
colorPickerDialog.setColorPickerDialogListener(
object : ColorPickerDialogListener {
override fun onDialogDismissed(dialogId: Int) {}
override fun onColorSelected(dialogId: Int, color: Int) {
b.typeColor.background.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
customColor = color
}
})
colorPickerDialog.show(activity.fragmentManager, "color-picker-dialog")
}
loadDates()
}}
@ -198,12 +288,20 @@ class EventManualV2Dialog(
val dates = deferred.await()
b.dateDropdown.clear().append(dates)
editingEvent?.let {
b.dateDropdown.select(it.eventDate.value.toLong())
editingEvent?.eventDate?.let {
b.dateDropdown.select(TextInputDropDown.Item(
it.value.toLong(),
it.formattedString,
tag = it
))
}
defaultLesson?.let {
b.dateDropdown.select(it.displayDate?.value?.toLong())
defaultLesson?.displayDate?.let {
b.dateDropdown.select(TextInputDropDown.Item(
it.value.toLong(),
it.formattedString,
tag = it
))
}
if (b.dateDropdown.selected == null) {
@ -216,22 +314,24 @@ class EventManualV2Dialog(
when {
// next lesson with specified subject
item.id < -1 -> {
app.db.timetableDao().getNextWithSubject(profileId, Date.getToday(), -item.id).observeOnce(activity, Observer {
val teamId = defaultLesson?.teamId ?: -1
val selectedLessonDate = defaultLesson?.date ?: Date.getToday()
when (teamId) {
-1L -> app.db.timetableDao().getNextWithSubject(profileId, selectedLessonDate, -item.id)
else -> app.db.timetableDao().getNextWithSubjectAndTeam(profileId, selectedLessonDate, -item.id, teamId)
}.observeOnce(activity, Observer {
val lessonDate = it?.displayDate ?: return@Observer
b.dateDropdown.selected = TextInputDropDown.Item(
b.dateDropdown.select(TextInputDropDown.Item(
lessonDate.value.toLong(),
lessonDate.formattedString,
tag = lessonDate
)
// TODO load correct hour when selecting next lesson
b.dateDropdown.updateText()
it.let {
b.teamDropdown.select(it.displayTeamId)
b.subjectDropdown.select(it.displaySubjectId)
b.teacherDropdown.select(it.displayTeacherId)
}
))
b.teamDropdown.select(it.displayTeamId)
b.subjectDropdown.select(it.displaySubjectId)
b.teacherDropdown.select(it.displayTeacherId)
defaultLoaded = false
loadHours()
loadHours(it.displayStartTime)
})
return@setOnChangeListener false
}
@ -239,17 +339,17 @@ class EventManualV2Dialog(
item.id == -1L -> {
MaterialDatePicker.Builder
.datePicker()
.setSelection((b.dateDropdown.selectedId?.let { Date.fromValue(it.toInt()) } ?: Date.getToday()).inMillis)
.setSelection((b.dateDropdown.selectedId?.let { Date.fromValue(it.toInt()) }
?: Date.getToday()).inMillis)
.build()
.apply {
addOnPositiveButtonClickListener {
val dateSelected = Date.fromMillis(it)
b.dateDropdown.selected = TextInputDropDown.Item(
b.dateDropdown.select(TextInputDropDown.Item(
dateSelected.value.toLong(),
dateSelected.formattedString,
tag = dateSelected
)
b.dateDropdown.updateText()
))
loadHours()
}
show(this@EventManualV2Dialog.activity.supportFragmentManager, "MaterialDatePicker")
@ -269,7 +369,7 @@ class EventManualV2Dialog(
loadHours()
}}
private fun loadHours() {
private fun loadHours(defaultHour: Time? = null) {
b.timeDropdown.isEnabled = false
// get the selected date
val date = b.dateDropdown.selectedId?.let { Date.fromValue(it.toInt()) } ?: return
@ -295,7 +395,8 @@ class EventManualV2Dialog(
lesson.displayStartTime?.stringHM ?: "",
lesson.displaySubjectName?.let {
when {
lesson.type == Lesson.TYPE_CANCELLED -> it.asStrikethroughSpannable()
lesson.type == Lesson.TYPE_CANCELLED
|| lesson.type == Lesson.TYPE_SHIFTED_SOURCE -> it.asStrikethroughSpannable()
lesson.type != Lesson.TYPE_NORMAL -> it.asItalicSpannable()
else -> it
}
@ -331,6 +432,10 @@ class EventManualV2Dialog(
defaultLesson?.let {
b.timeDropdown.select(it.displayStartTime?.value?.toLong())
}
defaultHour?.let {
b.timeDropdown.select(it.value.toLong())
}
}
defaultLoaded = true
b.timeDropdown.isEnabled = true
@ -338,16 +443,16 @@ class EventManualV2Dialog(
// attach a listener to time dropdown
b.timeDropdown.setOnChangeListener { item ->
when {
// custom start hour
item.id == -1L -> {
return@setOnChangeListener false
}
// no lessons this day
item.id == -2L -> {
b.timeDropdown.deselect()
return@setOnChangeListener false
}
// custom start hour
item.id == -1L -> {
return@setOnChangeListener false
}
// selected a specific lesson
else -> {
if (item.tag is LessonFull) {
@ -376,4 +481,4 @@ class EventManualV2Dialog(
private fun saveEvent() {
}
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-24.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.home
import android.text.InputType
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.afollestad.materialdialogs.MaterialDialog
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
class StudentNumberDialog(
val activity: AppCompatActivity,
val profile: Profile,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) {
companion object {
private const val TAG = "StudentNumberDialog"
}
private lateinit var dialog: AlertDialog
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
MaterialDialog.Builder(activity)
.title(R.string.card_lucky_number_set_title)
.content(R.string.card_lucky_number_set_text)
.inputType(InputType.TYPE_CLASS_NUMBER)
.input(null, if (profile.studentNumber == -1) "" else profile.studentNumber.toString()) { _: MaterialDialog?, input: CharSequence ->
try {
profile.studentNumber = input.toString().toInt()
} catch (e: Exception) {
Toast.makeText(activity, R.string.incorrect_format, Toast.LENGTH_SHORT).show()
}
}
.dismissListener {
onDismissListener?.invoke(TAG)
}.show()
}}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-13.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.settings
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding
import pl.szczodrzynski.edziennik.utils.models.Notification
import java.util.*
import kotlin.coroutines.CoroutineContext
class ProfileRemoveDialog(
val activity: MainActivity,
val profileId: Int,
val profileName: String
) : CoroutineScope {
companion object {
private const val TAG = "ProfileRemoveDialog"
}
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private val app by lazy { activity.application as App }
private lateinit var b: DialogLessonDetailsBinding
private lateinit var dialog: AlertDialog
init { run {
job = Job()
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.profile_menu_remove_confirm)
.setMessage(activity.getString(R.string.profile_menu_remove_confirm_text_format, profileName, profileName))
.setPositiveButton(R.string.remove) { _, _ ->
removeProfile()
}
.setNeutralButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
.setCancelable(false)
.show()
}}
private fun removeProfile() { launch {
val deferred = async(Dispatchers.Default) {
val profileObject = app.db.profileDao().getByIdNow(profileId) ?: return@async
app.db.announcementDao().clear(profileId)
app.db.attendanceDao().clear(profileId)
app.db.eventDao().clear(profileId)
app.db.eventTypeDao().clear(profileId)
app.db.gradeDao().clear(profileId)
app.db.gradeCategoryDao().clear(profileId)
app.db.lessonDao().clear(profileId)
app.db.lessonChangeDao().clear(profileId)
app.db.luckyNumberDao().clear(profileId)
app.db.noticeDao().clear(profileId)
app.db.subjectDao().clear(profileId)
app.db.teacherDao().clear(profileId)
app.db.teamDao().clear(profileId)
app.db.messageRecipientDao().clear(profileId)
app.db.messageDao().clear(profileId)
app.db.endpointTimerDao().clear(profileId)
app.db.attendanceTypeDao().clear(profileId)
app.db.classroomDao().clear(profileId)
app.db.lessonRangeDao().clear(profileId)
app.db.noticeTypeDao().clear(profileId)
app.db.teacherAbsenceDao().clear(profileId)
app.db.teacherAbsenceTypeDao().clear(profileId)
app.db.timetableDao().clear(profileId)
val loginStoreId = profileObject.loginStoreId
val profilesUsingLoginStore = app.db.profileDao().getIdsByLoginStoreIdNow(loginStoreId)
if (profilesUsingLoginStore.size == 1) {
app.db.loginStoreDao().remove(loginStoreId)
}
app.db.profileDao().remove(profileId)
app.db.metadataDao().deleteAll(profileId)
val toRemove = ArrayList<Notification>()
for (notification in app.appConfig.notifications) {
if (notification.profileId == profileId) {
toRemove.add(notification)
}
}
app.appConfig.notifications.removeAll(toRemove)
app.profile = null
App.profileId = -1
app.profileLoadById(app.profileLastId())
}
deferred.await()
dialog.dismiss()
activity.reloadTarget()
Toast.makeText(activity, R.string.dialog_profile_remove_success, Toast.LENGTH_LONG).show()
}}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-13.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.sync
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.api.v2.events.task.EdziennikTask
import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
import kotlin.coroutines.CoroutineContext
class SyncViewListDialog(
val activity: MainActivity,
val currentViewId: Int? = null
) : CoroutineScope {
companion object {
private const val TAG = "SyncViewListDialog"
}
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private val app by lazy { activity.application as App }
private lateinit var b: DialogLessonDetailsBinding
private lateinit var dialog: AlertDialog
init { run {
job = Job()
val viewIds = arrayOf(
MainActivity.DRAWER_ITEM_TIMETABLE,
MainActivity.DRAWER_ITEM_AGENDA,
MainActivity.DRAWER_ITEM_GRADES,
MainActivity.DRAWER_ITEM_HOMEWORK,
MainActivity.DRAWER_ITEM_BEHAVIOUR,
MainActivity.DRAWER_ITEM_ATTENDANCE,
MainActivity.DRAWER_ITEM_MESSAGES,
MainActivity.DRAWER_ITEM_MESSAGES,
MainActivity.DRAWER_ITEM_ANNOUNCEMENTS
)
val items = arrayOf<String>(
app.getString(R.string.menu_timetable),
app.getString(R.string.menu_agenda),
app.getString(R.string.menu_grades),
app.getString(R.string.menu_homework),
app.getString(R.string.menu_notices),
app.getString(R.string.menu_attendance),
app.getString(R.string.title_messages_inbox_single),
app.getString(R.string.title_messages_sent_single),
app.getString(R.string.menu_announcements)
)
val everything = currentViewId == MainActivity.DRAWER_ITEM_HOME
val checkedItems = booleanArrayOf(
everything || currentViewId == MainActivity.DRAWER_ITEM_TIMETABLE,
everything || currentViewId == MainActivity.DRAWER_ITEM_AGENDA,
everything || currentViewId == MainActivity.DRAWER_ITEM_GRADES,
everything || currentViewId == MainActivity.DRAWER_ITEM_HOMEWORK,
everything || currentViewId == MainActivity.DRAWER_ITEM_BEHAVIOUR,
everything || currentViewId == MainActivity.DRAWER_ITEM_ATTENDANCE,
everything || currentViewId == MainActivity.DRAWER_ITEM_MESSAGES && MessagesFragment.pageSelection != 1,
everything || currentViewId == MainActivity.DRAWER_ITEM_MESSAGES && MessagesFragment.pageSelection == 1,
everything || currentViewId == MainActivity.DRAWER_ITEM_ANNOUNCEMENTS
)
val userChooses = checkedItems.toMutableList()
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.dialog_sync_view_list_title)
.setMultiChoiceItems(items, checkedItems) { _, which, isChecked ->
userChooses[which] = isChecked
}
.setPositiveButton(R.string.ok) { _, _ ->
dialog.dismiss()
val selectedViewIds = userChooses.mapIndexed { index, it ->
if (it)
viewIds[index] to when (index) {
7 -> 1
else -> 0
}
else
null
}.let {
listOfNotNull(*it.toTypedArray())
}
if (selectedViewIds.isNotEmpty()) {
activity.swipeRefreshLayout.isRefreshing = true
EdziennikTask.syncProfile(
App.profileId,
selectedViewIds
).enqueue(activity)
}
}
.setNeutralButton(R.string.sync_feature_all) { _, _ ->
dialog.dismiss()
activity.swipeRefreshLayout.isRefreshing = true
EdziennikTask.syncProfile(App.profileId).enqueue(activity)
}
.setNegativeButton(R.string.cancel) { _, _ ->
dialog.dismiss()
}
.show()
}}
}

View File

@ -21,7 +21,9 @@ import pl.szczodrzynski.edziennik.utils.models.Week
class LessonDetailsDialog(
val activity: AppCompatActivity,
val lesson: LessonFull
val lesson: LessonFull,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) {
companion object {
private const val TAG = "LessonDetailsDialog"
@ -31,6 +33,9 @@ class LessonDetailsDialog(
private lateinit var dialog: AlertDialog
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
b = DialogLessonDetailsBinding.inflate(activity.layoutInflater)
dialog = MaterialAlertDialogBuilder(activity)
.setView(b.root)
@ -38,8 +43,13 @@ class LessonDetailsDialog(
dialog.dismiss()
}
.setNeutralButton(R.string.add) { dialog, _ ->
dialog.dismiss()
EventManualV2Dialog(activity, lesson.profileId, lesson)
EventManualV2Dialog(
activity,
lesson.profileId,
lesson,
onShowListener = onShowListener,
onDismissListener = onDismissListener
)
/*MaterialAlertDialogBuilder(activity)
.setItems(R.array.main_menu_add_options) { dialog2, which ->
dialog2.dismiss()
@ -59,6 +69,9 @@ class LessonDetailsDialog(
.setNegativeButton(R.string.cancel) { dialog2, _ -> dialog2.dismiss() }
.show()*/
}
.setOnDismissListener {
onDismissListener?.invoke(TAG)
}
.show()
update()
}}
@ -107,7 +120,7 @@ class LessonDetailsDialog(
dialog.dismiss()
val dateStr = otherLessonDate?.stringY_m_d ?: return@setOnClickListener
val intent = Intent(TimetableFragment.ACTION_SCROLL_TO_DATE).apply {
putExtra("date", dateStr)
putExtra("timetableDate", dateStr)
}
activity.sendBroadcast(intent)
}
@ -119,29 +132,29 @@ class LessonDetailsDialog(
if (lesson.type < Lesson.TYPE_SHIFTED_SOURCE && lesson.oldSubjectId != null && lesson.subjectId != lesson.oldSubjectId) {
b.oldSubjectName = lesson.oldSubjectName
}
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.subjectId != null) {
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displaySubjectId != null) {
b.subjectName = lesson.subjectName
}
if (lesson.type < Lesson.TYPE_SHIFTED_SOURCE && lesson.oldTeacherId != null && lesson.teacherId != lesson.oldTeacherId) {
b.oldTeacherName = lesson.oldTeacherName
}
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.teacherId != null) {
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displayTeacherId != null) {
b.teacherName = lesson.teacherName
}
if (lesson.oldClassroom != null && lesson.classroom != lesson.oldClassroom) {
b.oldClassroom = lesson.oldClassroom
}
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.classroom != null) {
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displayClassroom != null) {
b.classroom = lesson.classroom
}
if (lesson.type < Lesson.TYPE_SHIFTED_SOURCE && lesson.oldTeamId != null && lesson.teamId != lesson.oldTeamId) {
b.oldTeamName = lesson.oldTeamName
}
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.teamId != null) {
if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displayTeamId != null) {
b.teamName = lesson.teamName
}
}
}
}

View File

@ -6,15 +6,16 @@ import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import com.afollestad.materialdialogs.MaterialDialog;
import com.applandeo.materialcalendarview.CalendarView;
import com.applandeo.materialcalendarview.EventDay;
@ -27,6 +28,7 @@ import com.mikepenz.iconics.IconicsColor;
import com.mikepenz.iconics.IconicsDrawable;
import com.mikepenz.iconics.IconicsSize;
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial;
import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont;
import java.util.ArrayList;
import java.util.Calendar;
@ -34,13 +36,13 @@ import java.util.List;
import java.util.Locale;
import pl.szczodrzynski.edziennik.App;
import pl.szczodrzynski.edziennik.R;
import pl.szczodrzynski.edziennik.MainActivity;
import pl.szczodrzynski.edziennik.R;
import pl.szczodrzynski.edziennik.data.db.modules.events.EventFull;
import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonFull;
import pl.szczodrzynski.edziennik.data.db.modules.teachers.TeacherAbsenceFull;
import pl.szczodrzynski.edziennik.databinding.FragmentAgendaCalendarBinding;
import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding;
import pl.szczodrzynski.edziennik.data.db.modules.events.EventFull;
import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonFull;
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListDialog;
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog;
import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog;
@ -51,11 +53,11 @@ import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange.LessonChangeEve
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceCounter;
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEvent;
import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEventRenderer;
import pl.szczodrzynski.edziennik.utils.models.Date;
import pl.szczodrzynski.edziennik.utils.models.Time;
import pl.szczodrzynski.edziennik.utils.Colors;
import pl.szczodrzynski.edziennik.utils.Themes;
import pl.szczodrzynski.edziennik.utils.Utils;
import pl.szczodrzynski.edziennik.utils.models.Date;
import pl.szczodrzynski.edziennik.utils.models.Time;
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem;
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem;
@ -102,7 +104,7 @@ public class AgendaFragment extends Fragment {
new BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_add_event)
.withDescription(R.string.menu_add_event_desc)
.withIcon(CommunityMaterial.Icon.cmd_calendar_plus)
.withIcon(SzkolnyFont.Icon.szf_calendar_plus_outline)
.withOnClickListener(v3 -> {
activity.getBottomSheet().close();
new MaterialDialog.Builder(activity)
@ -122,7 +124,7 @@ public class AgendaFragment extends Fragment {
}),
new BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_agenda_change_view)
.withIcon(viewType == AGENDA_DEFAULT ? CommunityMaterial.Icon.cmd_calendar : CommunityMaterial.Icon2.cmd_view_list)
.withIcon(viewType == AGENDA_DEFAULT ? CommunityMaterial.Icon.cmd_calendar_outline : CommunityMaterial.Icon.cmd_format_list_bulleted_square)
.withOnClickListener(v3 -> {
activity.getBottomSheet().close();
viewType = viewType == AGENDA_DEFAULT ? AGENDA_CALENDAR : AGENDA_DEFAULT;
@ -133,7 +135,7 @@ public class AgendaFragment extends Fragment {
new BottomSheetSeparatorItem(true),
new BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_as_read)
.withIcon(CommunityMaterial.Icon.cmd_eye_check)
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
.withOnClickListener(v3 -> {
activity.getBottomSheet().close();
AsyncTask.execute(() -> app.db.metadataDao().setAllSeen(App.profileId, TYPE_EVENT, true));

View File

@ -58,7 +58,7 @@ public class AnnouncementsFragment extends Fragment {
activity.getBottomSheet().prependItems(
new BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_as_read)
.withIcon(CommunityMaterial.Icon.cmd_eye_check)
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
.withOnClickListener(v3 -> {
activity.getBottomSheet().close();
AsyncTask.execute(() -> app.db.metadataDao().setAllSeen(App.profileId, TYPE_ANNOUNCEMENT, true));

View File

@ -95,7 +95,7 @@ public class AttendanceFragment extends Fragment {
activity.getBottomSheet().prependItems(
new BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_as_read)
.withIcon(CommunityMaterial.Icon.cmd_eye_check)
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
.withOnClickListener(v3 -> {
activity.getBottomSheet().close();
AsyncTask.execute(() -> app.db.metadataDao().setAllSeen(App.profileId, TYPE_ATTENDANCE, true));
@ -133,7 +133,7 @@ public class AttendanceFragment extends Fragment {
CafeBar.builder(activity)
.to(activity.getNavView().getCoordinator())
.content(R.string.sync_old_data_info)
.icon(new IconicsDrawable(activity).icon(CommunityMaterial.Icon2.cmd_sync).size(IconicsSize.dp(20)).color(IconicsColor.colorInt(Themes.INSTANCE.getPrimaryTextColor(activity))))
.icon(new IconicsDrawable(activity).icon(CommunityMaterial.Icon.cmd_download_outline).size(IconicsSize.dp(20)).color(IconicsColor.colorInt(Themes.INSTANCE.getPrimaryTextColor(activity))))
.positiveText(R.string.refresh)
.positiveColor(0xff4caf50)
.negativeText(R.string.ok)

View File

@ -69,7 +69,7 @@ public class BehaviourFragment extends Fragment {
activity.getBottomSheet().prependItems(
new BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_as_read)
.withIcon(CommunityMaterial.Icon.cmd_eye_check)
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
.withOnClickListener(v3 -> {
activity.getBottomSheet().close();
AsyncTask.execute(() -> app.db.metadataDao().setAllSeen(App.profileId, TYPE_NOTICE, true));

View File

@ -9,21 +9,20 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.mikepenz.iconics.IconicsDrawable
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont
import com.mikepenz.iconics.utils.colorRes
import com.mikepenz.iconics.utils.sizeDp
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_MOBIDZIENNIK
import pl.szczodrzynski.edziennik.data.db.modules.notices.Notice
import pl.szczodrzynski.edziennik.data.db.modules.notices.NoticeFull
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.data.db.modules.login.LoginStore.LOGIN_TYPE_MOBIDZIENNIK
import pl.szczodrzynski.edziennik.utils.Utils.bs
import pl.szczodrzynski.edziennik.utils.models.Date
class NoticesAdapter//getting the context and product list with constructor
(private val context: Context, var noticeList: List<NoticeFull>) : RecyclerView.Adapter<NoticesAdapter.ViewHolder>() {
@ -50,15 +49,15 @@ class NoticesAdapter//getting the context and product list with constructor
holder.noticesItemAddedDate.text = Date.fromMillis(notice.addedDate).formattedString
if (notice.type == Notice.TYPE_POSITIVE) {
holder.noticesItemType.setImageDrawable(IconicsDrawable(context, CommunityMaterial.Icon2.cmd_plus_circle)
holder.noticesItemType.setImageDrawable(IconicsDrawable(context, CommunityMaterial.Icon2.cmd_plus_circle_outline)
.colorRes(R.color.md_green_600)
.sizeDp(36))
} else if (notice.type == Notice.TYPE_NEGATIVE) {
holder.noticesItemType.setImageDrawable(IconicsDrawable(context, CommunityMaterial.Icon.cmd_alert_decagram)
holder.noticesItemType.setImageDrawable(IconicsDrawable(context, CommunityMaterial.Icon.cmd_alert_decagram_outline)
.colorRes(R.color.md_red_600)
.sizeDp(36))
} else {
holder.noticesItemType.setImageDrawable(IconicsDrawable(context, CommunityMaterial.Icon2.cmd_message_outline)
holder.noticesItemType.setImageDrawable(IconicsDrawable(context, SzkolnyFont.Icon.szf_message_processing_outline)
.colorRes(R.color.md_blue_500)
.sizeDp(36))
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-13.
*/
package pl.szczodrzynski.edziennik.ui.modules.error
import android.util.Log
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding
import kotlin.coroutines.CoroutineContext
class ErrorDialog(
val activity: AppCompatActivity,
val exception: Exception
) : CoroutineScope {
companion object {
private const val TAG = "ErrorDialog"
}
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private val app by lazy { activity.application as App }
private lateinit var b: DialogLessonDetailsBinding
private lateinit var dialog: AlertDialog
init { run {
job = Job()
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.error_occured)
.setMessage(exception.message + "\n" + Log.getStackTraceString(exception))
.setPositiveButton(R.string.ok) { _, _ ->
dialog.dismiss()
}
.show()
}}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-13.
*/
package pl.szczodrzynski.edziennik.ui.modules.error
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.ColorUtils
import com.google.android.material.snackbar.Snackbar
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.api.v2.models.ApiError
import pl.szczodrzynski.navlib.getColorFromAttr
class ErrorSnackbar(val activity: AppCompatActivity) {
companion object {
private const val TAG = "ErrorSnackbar"
}
private var snackbar: Snackbar? = null
private lateinit var coordinator: CoordinatorLayout
private val errors = mutableListOf<ApiError>()
fun setCoordinator(coordinatorLayout: CoordinatorLayout, showAbove: View? = null) {
this.coordinator = coordinatorLayout
snackbar = Snackbar.make(coordinator, R.string.snackbar_error_text, Snackbar.LENGTH_INDEFINITE)
snackbar?.setAction(R.string.more) {
}
val bgColor = ColorUtils.compositeColors(
getColorFromAttr(activity, R.attr.colorOnSurface) and 0xcfffffff.toInt(),
getColorFromAttr(activity, R.attr.colorSurface)
)
snackbar?.setBackgroundTint(bgColor)
showAbove?.let { snackbar?.anchorView = it }
}
fun addError(apiError: ApiError): ErrorSnackbar {
errors += apiError
snackbar?.setText(apiError.getStringReason(activity))
return this
}
fun show() = snackbar?.show()
fun dismiss() = snackbar?.dismiss()
}

View File

@ -132,7 +132,7 @@ public class GradesFragment extends Fragment {
}),
new BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_grades_color_mode)
.withIcon(CommunityMaterial.Icon2.cmd_palette)
.withIcon(CommunityMaterial.Icon2.cmd_palette_outline)
.withOnClickListener(v3 -> {
activity.getBottomSheet().close();
new MaterialDialog.Builder(activity)
@ -195,7 +195,7 @@ public class GradesFragment extends Fragment {
new BottomSheetSeparatorItem(true),
new BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_as_read)
.withIcon(CommunityMaterial.Icon.cmd_eye_check)
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
.withOnClickListener(v3 -> {
activity.getBottomSheet().close();
AsyncTask.execute(() -> app.db.metadataDao().setAllSeen(App.profileId, TYPE_GRADE, true));

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-23.
*/
package pl.szczodrzynski.edziennik.ui.modules.home
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragmentV2.Companion.swapCards
import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
class CardItemTouchHelperCallback(private val cardAdapter: HomeCardAdapter, private val refreshLayout: SwipeRefreshLayoutNoIndicator?) : ItemTouchHelper.Callback() {
companion object {
private const val TAG = "CardItemTouchHelperCallback"
private const val DRAG_FLAGS = ItemTouchHelper.UP or ItemTouchHelper.DOWN
private const val SWIPE_FLAGS = 0
}
private var dragCardView: MaterialCardView? = null
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return makeMovementFlags(DRAG_FLAGS, SWIPE_FLAGS)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
val fromPosition = viewHolder.adapterPosition
val toPosition = target.adapterPosition
swapCards(fromPosition, toPosition, cardAdapter)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) {
dragCardView = viewHolder.itemView as MaterialCardView
dragCardView?.isDragged = true
refreshLayout?.isEnabled = false
} else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE && dragCardView != null) {
refreshLayout?.isEnabled = true
dragCardView?.isDragged = false
dragCardView = null
}
}
}

View File

@ -1,11 +1,12 @@
package pl.szczodrzynski.edziennik.ui.modules.home;
import androidx.databinding.DataBindingUtil;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import androidx.databinding.DataBindingUtil;
import java.util.ArrayList;
import java.util.List;
@ -14,8 +15,8 @@ import java.util.TimerTask;
import pl.szczodrzynski.edziennik.App;
import pl.szczodrzynski.edziennik.R;
import pl.szczodrzynski.edziennik.databinding.ActivityCounterBinding;
import pl.szczodrzynski.edziennik.data.db.modules.lessons.LessonFull;
import pl.szczodrzynski.edziennik.databinding.ActivityCounterBinding;
import pl.szczodrzynski.edziennik.utils.models.Date;
import pl.szczodrzynski.edziennik.utils.models.Time;
@ -88,7 +89,7 @@ public class CounterActivity extends AppCompatActivity {
private void findLessons(Time syncedNow) {
AsyncTask.execute(() -> {
Date today = Date.getToday();
lessons = app.db.lessonDao().getAllNearestNow(App.profileId, today.clone().stepForward(0, 0, -today.getWeekDay()), today, syncedNow);
lessons = app.db.lessonDao().getAllNearestNow(App.profileId, today.getWeekStart(), today, syncedNow);
if (lessons != null && lessons.size() != 0) {
Date displayingDate = lessons.get(0).lessonDate;

View File

@ -0,0 +1,10 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-23.
*/
package pl.szczodrzynski.edziennik.ui.modules.home
interface HomeCard {
fun bind(position: Int, holder: HomeCardAdapter.ViewHolder)
fun unbind(position: Int, holder: HomeCardAdapter.ViewHolder)
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-23.
*/
package pl.szczodrzynski.edziennik.ui.modules.home
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
import pl.szczodrzynski.edziennik.R
class HomeCardAdapter(var items: MutableList<HomeCard>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
private const val TAG = "HomeCardAdapter"
}
var itemTouchHelper: ItemTouchHelper? = null
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as ViewHolder).bind(itemTouchHelper)
items[position].bind(position, holder)
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
items.getOrNull(holder.adapterPosition)?.unbind(holder.adapterPosition, holder as ViewHolder)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.card_home, parent, false) as MaterialCardView
)
}
override fun getItemCount(): Int = items.size
class ViewHolder(val root: MaterialCardView) : RecyclerView.ViewHolder(root) {
@SuppressLint("ClickableViewAccessibility")
fun bind(itemTouchHelper: ItemTouchHelper?) {
/*root.setOnTouchListener { _: View?, event: MotionEvent ->
if (event.action == MotionEvent.ACTION_DOWN) {
itemTouchHelper?.startDrag(this)
return@setOnTouchListener true
}
false
}*/
}
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-23.
*/
package pl.szczodrzynski.edziennik.ui.modules.home
import android.widget.TextView
import androidx.core.view.plusAssign
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.startCoroutineTimer
import kotlin.coroutines.CoroutineContext
class HomeDummyCard(val id: Int) : HomeCard, CoroutineScope {
companion object {
private const val TAG = "HomeDummyCard"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private var job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
var timer: Job? = null
var time = 0
override fun bind(position: Int, holder: HomeCardAdapter.ViewHolder) { launch {
holder.root.removeAllViews()
//holder.setIsRecyclable(false)
val text = TextView(holder.root.context).apply {
text = "This is a card #$id"
}
holder.root += text
timer = startCoroutineTimer(repeatMillis = 1000) {
time++
text.text = "Coroutine timer at #$id! $time seconds"
}
/*val button = MaterialButton(holder.root.context).apply {
setText("Cancel")
onClick {
timer.cancel()
}
}
holder.root += button*/
}}
override fun unbind(position: Int, holder: HomeCardAdapter.ViewHolder) {
timer?.cancel()
timer = null
}
}

View File

@ -37,6 +37,7 @@ import com.mikepenz.iconics.IconicsColor;
import com.mikepenz.iconics.IconicsDrawable;
import com.mikepenz.iconics.IconicsSize;
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial;
import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont;
import java.io.File;
import java.util.ArrayList;
@ -268,7 +269,7 @@ public class HomeFragment extends Fragment {
activity.getBottomSheet().prependItems(
new BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_set_student_number)
.withIcon(CommunityMaterial.Icon.cmd_counter)
.withIcon(SzkolnyFont.Icon.szf_clipboard_list_outline)
.withOnClickListener(v3 -> {
activity.getBottomSheet().close();
setNumberDialog();
@ -276,7 +277,7 @@ public class HomeFragment extends Fragment {
new BottomSheetSeparatorItem(true),
new BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_everything_as_read)
.withIcon(CommunityMaterial.Icon.cmd_eye_check)
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
.withOnClickListener(v3 -> {
activity.getBottomSheet().close();
AsyncTask.execute(() -> app.db.metadataDao().setAllSeen(App.profileId, true));

View File

@ -0,0 +1,141 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-23.
*/
package pl.szczodrzynski.edziennik.ui.modules.home
import android.os.AsyncTask
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.OnClickListener
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon
import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.FragmentHomeV2Binding
import pl.szczodrzynski.edziennik.ui.dialogs.home.StudentNumberDialog
import pl.szczodrzynski.edziennik.ui.modules.home.cards.HomeLuckyNumberCard
import pl.szczodrzynski.edziennik.ui.modules.home.cards.HomeTimetableCard
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import kotlin.coroutines.CoroutineContext
class HomeFragmentV2 : Fragment(), CoroutineScope {
companion object {
private const val TAG = "HomeFragment"
fun swapCards(fromPosition: Int, toPosition: Int, cardAdapter: HomeCardAdapter) {
val fromCard = cardAdapter.items[fromPosition]
cardAdapter.items[fromPosition] = cardAdapter.items[toPosition]
cardAdapter.items[toPosition] = fromCard
cardAdapter.notifyItemMoved(fromPosition, toPosition)
}
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: FragmentHomeV2Binding
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true)
b = FragmentHomeV2Binding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
job = Job()
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
return
activity.bottomSheet.prependItems(
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_set_student_number)
.withIcon(SzkolnyFont.Icon.szf_clipboard_list_outline)
.withOnClickListener(OnClickListener {
activity.bottomSheet.close()
StudentNumberDialog(activity, app.profile) {
app.profileSaveAsync()
}
}),
BottomSheetSeparatorItem(true),
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_everything_as_read)
.withIcon(Icon.cmd_eye_check_outline)
.withOnClickListener(OnClickListener {
activity.bottomSheet.close()
AsyncTask.execute { app.db.metadataDao().setAllSeen(App.profileId, true) }
Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show()
})
)
val items = mutableListOf<HomeCard>(
HomeLuckyNumberCard(0, app, activity, this, app.profile),
HomeTimetableCard(1, app, activity, this, app.profile)
)
val adapter = HomeCardAdapter(items)
val itemTouchHelper = ItemTouchHelper(CardItemTouchHelperCallback(adapter, b.refreshLayout))
adapter.itemTouchHelper = itemTouchHelper
b.list.layoutManager = LinearLayoutManager(activity)
b.list.adapter = adapter
b.list.setAccessibilityDelegateCompat(object : RecyclerViewAccessibilityDelegate(b.list) {
override fun getItemDelegate(): AccessibilityDelegateCompat {
return object : ItemDelegate(this) {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
val position: Int = b.list.getChildLayoutPosition(host)
if (position != 0) {
info.addAction(AccessibilityActionCompat(
R.id.move_card_up_action,
host.resources.getString(R.string.card_action_move_up)
))
}
if (position != adapter.itemCount - 1) {
info.addAction(AccessibilityActionCompat(
R.id.move_card_down_action,
host.resources.getString(R.string.card_action_move_down)
))
}
}
override fun performAccessibilityAction(host: View, action: Int, args: Bundle): Boolean {
val fromPosition: Int = b.list.getChildLayoutPosition(host)
if (action == R.id.move_card_down_action) {
swapCards(fromPosition, fromPosition + 1, adapter)
return true
} else if (action == R.id.move_card_up_action) {
swapCards(fromPosition, fromPosition - 1, adapter)
return true
}
return super.performAccessibilityAction(host, action, args)
}
}
}
})
itemTouchHelper.attachToRecyclerView(b.list)
}
}

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