Compare commits

...

68 Commits

Author SHA1 Message Date
1e7dbba995 [4.13.7] Update build.gradle, signing and changelog 2024-07-08 14:10:29 +02:00
0b8f3fe94b [Actions] Add build fixes from develop 2024-07-08 14:10:23 +02:00
0e8b0673ca [App] Add Demo login method 2024-07-08 13:28:54 +02:00
cefb0deba8 [Actions] Rename changelog output name. 2023-03-25 10:09:36 +01:00
90a151c129 [4.13.6] Update build.gradle, signing and changelog. 2023-03-24 22:27:27 +01:00
9fd9721ae7 [UI] Hide Debugging menu without dev mode. 2023-03-24 22:24:33 +01:00
ceca75ef4b [UI/Timetable] Add option to sync current week. 2023-03-24 22:16:34 +01:00
21c00bbe53 [API] Fix detecting session cookies. Remove expired cookies. 2023-03-24 22:09:31 +01:00
db00566ebf [UI/Login] Fallback reCAPTCHA to WebView activity. 2023-03-24 22:09:31 +01:00
07ab1b984f [API/Librus] Fix login. (#176) 2023-03-24 22:09:03 +01:00
8177d4aa2d [Widgets] Fix pending intents mutability. Hide timetable sync button. 2023-03-24 11:13:00 +01:00
beff1b6460 [App] Fix cookie persistence. 2023-03-24 10:56:35 +01:00
31b569b02e [4.13.5] Update build.gradle, signing and changelog. 2023-03-22 23:16:28 +01:00
8bf77817d2 [UI] Fix writing files on Android 13 and newer. 2023-03-22 23:15:45 +01:00
27b61adf1d [Actions] Fix Play release publishing workflow. 2022-12-27 12:30:03 +01:00
a0244841ad [4.13.4] Update build.gradle, signing and changelog. 2022-12-26 14:45:29 +01:00
12c0c6f2ec [UI] Always show event subject dropdown for university school. 2022-12-26 14:43:42 +01:00
aaa3b8626e [UI] Update event types for university school. 2022-12-26 14:01:25 +01:00
48c9e2dfe3 [4.13.3] Update build.gradle, signing and changelog. 2022-12-06 10:35:23 +01:00
81d4801d27 [UI] Add snowfall to CounterActivity. Enable in February as well. 2022-12-06 10:34:12 +01:00
5f8016061d [API/Vulcan] Fix wrong serializing of null in JSON causing API error. 2022-12-06 10:22:47 +01:00
5007587192 [UI/Agenda] Allow prioritizing event subject over event type. 2022-11-30 11:29:43 +01:00
dfd1083e41 [UI/Timetable] Show lesson replacing notes in all places. 2022-11-30 10:41:43 +01:00
678baf46e5 [4.13.2] Update build.gradle, signing and changelog. 2022-11-28 20:30:11 +01:00
4077fe448d [4.13.2-rc.4] Update build.gradle, signing and changelog. 2022-11-25 16:52:16 +01:00
f085e17ef7 [API/Vulcan] Once again fix ignoring 404 response on Addressbook. 2022-11-25 16:51:35 +01:00
7fd2cad46b [4.13.2-rc.3] Update build.gradle, signing and changelog. 2022-11-25 16:13:58 +01:00
93dc2ac9ab [API/Vulcan] Fix ignoring 404 response on Addressbook. 2022-11-25 16:11:50 +01:00
ac53e267fc [4.13.2-rc.2] Update build.gradle, signing and changelog. 2022-11-25 14:55:49 +01:00
86eb1a0f42 [API/Vulcan] Actually ignore 404 response on Addressbook. 2022-11-25 14:54:05 +01:00
710d82da27 [4.13.2-rc.1] Update build.gradle, signing and changelog. 2022-11-25 14:40:39 +01:00
0123f50810 [API/Vulcan] Ignore 404 response on Addressbook. 2022-11-25 14:20:22 +01:00
6d3eb65445 [API/Mobidziennik] Do not clear email field if not set. 2022-11-15 22:20:00 +01:00
a9a0630226 [4.13.1] Update build.gradle, signing and changelog. 2022-11-03 22:59:40 +01:00
ec7577f999 [App] Revert to use old devMode config key. 2022-11-03 22:29:22 +01:00
05c7c0012c [UI] Fix home cards order not saving. 2022-11-03 22:28:59 +01:00
d65c6db954 [API/Librus] Fix getting read date in messages for multiple receivers. (#154) 2022-11-03 21:53:01 +01:00
771dc437e6 [Strings] Translate home timetable card "all lessons" to English. (#152) 2022-10-30 12:45:27 +01:00
3d5d3847cc [API/Librus] Fix getting teacher name in notices. (#151) 2022-10-30 12:44:59 +01:00
18cc60a80b [4.13] Update build.gradle, signing and changelog. 2022-10-26 20:55:50 +02:00
fedde9f739 [UI/Home] Show all next lessons before school day start. 2022-10-26 20:34:48 +02:00
9fde97bef0 [App] Share Lesson and Event notes to specific team only. 2022-10-26 11:14:32 +02:00
742bd03e9e [Lab] Fix JSON page crashing because of serializing AppDb. 2022-10-26 10:31:03 +02:00
62ffc652ab [4.13-rc.5] Update build.gradle, signing and changelog. 2022-10-25 20:50:19 +02:00
bfd2e9883a [App] Refactor getting profile config. 2022-10-25 20:48:10 +02:00
00e077d01f [UI] Fix notes not showing in note list dialog. 2022-10-25 20:07:48 +02:00
c21d89cf60 [UI] Fix SettingsAboutCard having duplicate items. 2022-10-25 19:58:26 +02:00
f52cc1b197 [UI] Make shared notes for lessons use a stable ID. 2022-10-25 19:58:09 +02:00
c90ad97f55 [UI] Remove "enable shared events" setting. Reorder settings a bit. 2022-10-25 17:33:38 +02:00
845e09d875 [API/Usos] Add prefixes to classroom and building names. 2022-10-25 12:36:11 +02:00
158b69a8d3 [Lab] Fix full sync buttons. 2022-10-25 12:35:51 +02:00
9535f53563 [App] Refactor profile methods as extensions. 2022-10-25 12:19:59 +02:00
eeb3fc4621 [4.13-rc.4] Update build.gradle, signing and changelog. 2022-10-24 23:33:54 +02:00
41693a9fc8 [App] Respect user setting before notifying about updates. 2022-10-24 23:33:02 +02:00
d3599b8c89 [UI] Fix DateDropdown next week Friday not visible. 2022-10-24 23:32:21 +02:00
ffd81f8b82 [UI] Add setting to share events/notes by default. 2022-10-24 23:32:09 +02:00
2c34924052 [UI] Mark Firebase-received events as manual. Update legend icons. 2022-10-24 22:41:15 +02:00
26ad6373e6 [4.13-rc.3] Update build.gradle, signing and changelog. 2022-10-23 23:16:23 +02:00
cac8f94407 [Gradle] Update Chucker to fix Android 12 crash issue. 2022-10-23 23:16:08 +02:00
6628b97faf [API/Usos] Fix detecting term start and end date. 2022-10-23 23:11:49 +02:00
8424414317 [Login] Fix configOverrides NPE during login. 2022-10-23 23:11:20 +02:00
d8bb927703 [4.13-rc.2] Update build.gradle, signing and changelog. 2022-10-22 22:34:24 +02:00
c8e8c172a2 [App] Rework update handling. 2022-10-22 22:10:04 +02:00
0d4dee765a [Lab] Allow setting custom API key. 2022-10-22 12:56:15 +02:00
fd407b2b03 [Config] Set highest data version by default. 2022-10-22 12:31:40 +02:00
40ed5a221f [App] Fix crashes while deserializing AppData and Config. 2022-10-22 12:19:23 +02:00
5150467372 [4.13-rc.1] Update build.gradle, signing and changelog. 2022-10-22 00:02:57 +02:00
63c5720f63 [App] Move per-register settings to JSON resource. Rewrite Config to use delegates. (#150)
* [App] Add base for AppData loading.

* [UI] Fix timetable widget date navigation.

* [UI] Migrate register-specific behavior to use AppData.

* [App] Implement new delegate-based config base.

* [Config] Migrate config and profile config.

* [Config] Remove defaults from migrations.

* [App] Apply event types and config overrides from AppData.

* [Events] Change default event types for university type school.
2022-10-21 23:59:53 +02:00
196 changed files with 5668 additions and 2271 deletions

View File

@ -23,8 +23,6 @@ def get_password(
auth_plugin="mysql_native_password", auth_plugin="mysql_native_password",
) )
print(f"Generating passwords for version {version_name} ({version_code})")
password = base64.b64encode(secrets.token_bytes(16)).decode() password = base64.b64encode(secrets.token_bytes(16)).decode()
iv = secrets.token_bytes(16) iv = secrets.token_bytes(16)

View File

@ -102,7 +102,9 @@ def get_commit_log(project_dir: str, format: str, max_lines: int = None) -> str:
) )
log = subprocess.run( log = subprocess.run(
args=f"git log {last_tag}..HEAD --format=%an%x00%at%x00%h%x00%s%x00%D".split(" "), args=f"git log {last_tag}..HEAD --format=%an%x00%at%x00%h%x00%s%x00%D".split(
" "
),
cwd=project_dir, cwd=project_dir,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
) )

View File

@ -1,11 +1,8 @@
import json
import os import os
import re import re
import sys import sys
from datetime import datetime, timedelta from datetime import datetime, timedelta
import requests
from _utils import ( from _utils import (
get_commit_log, get_commit_log,
get_project_dir, get_project_dir,
@ -25,17 +22,6 @@ if __name__ == "__main__":
print("Missing GitHub environment variables.") print("Missing GitHub environment variables.")
exit(-1) exit(-1)
with requests.get(
f"https://api.github.com/repos/{repo}/actions/runs?per_page=5&status=success"
) as r:
data = json.loads(r.text)
runs = [run for run in data["workflow_runs"] if run["head_sha"] == sha]
if runs:
print("::set-output name=hasNewChanges::false")
exit(0)
print("::set-output name=hasNewChanges::true")
project_dir = get_project_dir() project_dir = get_project_dir()
(version_code, version_name) = read_gradle_version(project_dir) (version_code, version_name) = read_gradle_version(project_dir)
@ -48,8 +34,8 @@ if __name__ == "__main__":
date -= timedelta(days=1) date -= timedelta(days=1)
version_name += "+nightly." + date.strftime("%Y%m%d") version_name += "+nightly." + date.strftime("%Y%m%d")
print("::set-output name=appVersionName::" + version_name) print("appVersionName=" + version_name)
print("::set-output name=appVersionCode::" + str(version_code)) print("appVersionCode=" + str(version_code))
write_gradle_version(project_dir, version_code, version_name) write_gradle_version(project_dir, version_code, version_name)

23
.github/utils/check_nightly.py vendored Normal file
View File

@ -0,0 +1,23 @@
import json
import os
import requests
if __name__ == "__main__":
repo = os.getenv("GITHUB_REPOSITORY")
sha = os.getenv("GITHUB_SHA")
if not repo or not sha:
print("Missing GitHub environment variables.")
exit(-1)
with requests.get(
f"https://api.github.com/repos/{repo}/actions/runs?per_page=5&status=success"
) as r:
data = json.loads(r.text)
runs = [run for run in data["workflow_runs"] if run["head_sha"] == sha]
if runs:
print("hasNewChanges=false")
exit(0)
print("hasNewChanges=true")

View File

@ -12,24 +12,24 @@ if __name__ == "__main__":
(version_code, version_name) = read_gradle_version(project_dir) (version_code, version_name) = read_gradle_version(project_dir)
print("::set-output name=appVersionName::" + version_name) print("appVersionName=" + version_name)
print("::set-output name=appVersionCode::" + str(version_code)) print("appVersionCode=" + str(version_code))
dir = f"{project_dir}/app/release/whatsnew-{version_name}/" dir = f"{project_dir}/app/release/whatsnew-{version_name}/"
os.makedirs(dir, exist_ok=True) os.makedirs(dir, exist_ok=True)
print("::set-output name=changelogDir::" + dir) print("changelogDir=" + dir)
(title, changelog) = get_changelog(project_dir, format="plain") (title, changelog) = get_changelog(project_dir, format="plain")
# plain text changelog - Firebase App Distribution # plain text changelog - Firebase App Distribution
with open(dir + "whatsnew-titled.txt", "w", encoding="utf-8") as f: with open(dir + "whatsnew_titled.txt", "w", encoding="utf-8") as f:
f.write(title) f.write(title)
f.write("\n") f.write("\n")
f.write(changelog) f.write(changelog)
print("::set-output name=changelogPlainTitledFile::" + dir + "whatsnew-titled.txt") print("changelogPlainTitledFile=" + dir + "whatsnew_titled.txt")
print("::set-output name=changelogTitle::" + title) print("changelogTitle=" + title)
# plain text changelog, max 500 chars - Google Play # plain text changelog, max 500 chars - Google Play
with open(dir + "whatsnew-pl-PL", "w", encoding="utf-8") as f: with open(dir + "whatsnew-pl-PL", "w", encoding="utf-8") as f:
@ -41,32 +41,31 @@ if __name__ == "__main__":
changelog = changelog.strip() changelog = changelog.strip()
f.write(changelog) f.write(changelog)
print("::set-output name=changelogPlainFile::" + dir + "whatsnew-pl-PL") print("changelogPlainFile=" + dir + "whatsnew-pl-PL")
# markdown changelog - Discord webhook # markdown changelog - Discord webhook
(_, changelog) = get_changelog(project_dir, format="markdown") (_, changelog) = get_changelog(project_dir, format="markdown")
with open(dir + "whatsnew.md", "w", encoding="utf-8") as f: with open(dir + "whatsnew.md", "w", encoding="utf-8") as f:
f.write(changelog) f.write(changelog)
print("::set-output name=changelogMarkdownFile::" + dir + "whatsnew.md") print("changelogMarkdownFile=" + dir + "whatsnew.md")
# html changelog - version info in DB # html changelog - version info in DB
(_, changelog) = get_changelog(project_dir, format="html") (_, changelog) = get_changelog(project_dir, format="html")
with open(dir + "whatsnew.html", "w", encoding="utf-8") as f: with open(dir + "whatsnew.html", "w", encoding="utf-8") as f:
f.write(changelog) f.write(changelog)
print("::set-output name=changelogHtmlFile::" + dir + "whatsnew.html") print("changelogHtmlFile=" + dir + "whatsnew.html")
changelog = get_commit_log(project_dir, format="plain", max_lines=10) changelog = get_commit_log(project_dir, format="plain", max_lines=10)
with open(dir + "commit_log.txt", "w", encoding="utf-8") as f: with open(dir + "commit_log.txt", "w", encoding="utf-8") as f:
f.write(changelog) f.write(changelog)
print("::set-output name=commitLogPlainFile::" + dir + "commit_log.txt") print("commitLogPlainFile=" + dir + "commit_log.txt")
changelog = get_commit_log(project_dir, format="markdown", max_lines=10) changelog = get_commit_log(project_dir, format="markdown", max_lines=10)
with open(dir + "commit_log.md", "w", encoding="utf-8") as f: with open(dir + "commit_log.md", "w", encoding="utf-8") as f:
f.write(changelog) f.write(changelog)
print("::set-output name=commitLogMarkdownFile::" + dir + "commit_log.md") print("commitLogMarkdownFile=" + dir + "commit_log.md")
changelog = get_commit_log(project_dir, format="html", max_lines=10) changelog = get_commit_log(project_dir, format="html", max_lines=10)
with open(dir + "commit_log.html", "w", encoding="utf-8") as f: with open(dir + "commit_log.html", "w", encoding="utf-8") as f:
f.write(changelog) f.write(changelog)
print("::set-output name=commitLogHtmlFile::" + dir + "commit_log.html") print("commitLogHtmlFile=" + dir + "commit_log.html")

View File

@ -13,7 +13,7 @@ if __name__ == "__main__":
files = glob.glob(f"{project_dir}/app/release/*.*") files = glob.glob(f"{project_dir}/app/release/*.*")
for file in files: for file in files:
file_relative = file.replace(os.getenv("GITHUB_WORKSPACE") + "/", "") file_relative = file.replace(project_dir + "/", "")
if "-aligned.apk" in file: if "-aligned.apk" in file:
os.unlink(file) os.unlink(file)
elif "-signed.apk" in file: elif "-signed.apk" in file:
@ -22,5 +22,5 @@ if __name__ == "__main__":
os.unlink(new_file) os.unlink(new_file)
os.rename(file, new_file) os.rename(file, new_file)
elif ".apk" in file or ".aab" in file: elif ".apk" in file or ".aab" in file:
print("::set-output name=signedReleaseFile::" + file) print("signedReleaseFile=" + file)
print("::set-output name=signedReleaseFileRelative::" + file_relative) print("signedReleaseFileRelative=" + file_relative)

View File

@ -64,7 +64,14 @@ def save_version(
if build_type in ["nightly", "daily"]: if build_type in ["nightly", "daily"]:
download_url = apk_server_nightly + apk_name if apk_name else None download_url = apk_server_nightly + apk_name if apk_name else None
else: else:
download_url = apk_server_release + apk_name if apk_name else None # download_url = apk_server_release + apk_name if apk_name else None
download_url = (
f"https://github.com/szkolny-eu/szkolny-android/releases/download/v{version_name}/{apk_name}"
if apk_name
else None
)
if download_url:
print("downloadUrl=" + download_url)
cols = [ cols = [
"versionCode", "versionCode",
@ -119,4 +126,12 @@ if __name__ == "__main__":
APK_SERVER_RELEASE = os.getenv("APK_SERVER_RELEASE") APK_SERVER_RELEASE = os.getenv("APK_SERVER_RELEASE")
APK_SERVER_NIGHTLY = os.getenv("APK_SERVER_NIGHTLY") APK_SERVER_NIGHTLY = os.getenv("APK_SERVER_NIGHTLY")
save_version(project_dir, DB_HOST, DB_USER, DB_PASS, DB_NAME, APK_SERVER_RELEASE, APK_SERVER_NIGHTLY) save_version(
project_dir,
DB_HOST,
DB_USER,
DB_PASS,
DB_NAME,
APK_SERVER_RELEASE,
APK_SERVER_NIGHTLY,
)

View File

@ -31,8 +31,6 @@ def sign(
SIGNING_FORMAT = "$param1.{}.$param2" SIGNING_FORMAT = "$param1.{}.$param2"
CPP_FORMAT = "/*{}*/\nstatic toys AES_IV[16] = {{\n\t{} }};" CPP_FORMAT = "/*{}*/\nstatic toys AES_IV[16] = {{\n\t{} }};"
print(f"Writing passwords for version {version_name} ({version_code})")
iv_hex = " ".join(["{:02x}".format(x) for x in iv]) iv_hex = " ".join(["{:02x}".format(x) for x in iv])
iv_cpp = ", ".join(["0x{:02x}".format(x) for x in iv]) iv_cpp = ", ".join(["0x{:02x}".format(x) for x in iv])
@ -71,8 +69,8 @@ if __name__ == "__main__":
version_name, version_code, DB_HOST, DB_USER, DB_PASS, DB_NAME version_name, version_code, DB_HOST, DB_USER, DB_PASS, DB_NAME
) )
print("::set-output name=appVersionName::" + version_name) print("appVersionName=" + version_name)
print("::set-output name=appVersionCode::" + str(version_code)) print("appVersionCode=" + str(version_code))
sign( sign(
project_dir, project_dir,

View File

@ -11,8 +11,7 @@ from _utils import get_changelog, get_commit_log, get_project_dir, read_gradle_v
def post_webhook( def post_webhook(
project_dir: str, project_dir: str,
apk_file: str, apk_file: str,
apk_server_release: str, download_url: str,
apk_server_nightly: str,
webhook_release: str, webhook_release: str,
webhook_testing: str, webhook_testing: str,
): ):
@ -25,12 +24,6 @@ def post_webhook(
testing = ["dev", "beta", "nightly", "daily"] testing = ["dev", "beta", "nightly", "daily"]
testing = build_type in testing testing = build_type in testing
apk_name = os.path.basename(apk_file)
if build_type in ["nightly", "daily"]:
download_url = apk_server_nightly + apk_name
else:
download_url = apk_server_release + apk_name
if testing: if testing:
build_date = int(os.stat(apk_file).st_mtime) build_date = int(os.stat(apk_file).st_mtime)
if build_date: if build_date:
@ -48,13 +41,17 @@ def post_webhook(
requests.post(url=webhook_testing, json=webhook) requests.post(url=webhook_testing, json=webhook)
else: else:
changelog = get_changelog(project_dir, format="markdown") changelog = get_changelog(project_dir, format="markdown")
webhook = get_webhook_release(changelog, download_url) webhook = get_webhook_release(version_name, changelog, download_url)
requests.post(url=webhook_release, json=webhook) requests.post(url=webhook_release, json=webhook)
def get_webhook_release(changelog: str, download_url: str): def get_webhook_release(version_name: str, changelog: str, download_url: str):
(title, content) = changelog (title, content) = changelog
return {"content": f"__**{title}**__\n{content}\n{download_url}"} return {
"content": (
f"__**{title}**__\n{content}\n[Szkolny.eu {version_name}]({download_url})"
),
}
def get_webhook_testing( def get_webhook_testing(
@ -73,9 +70,11 @@ def get_webhook_testing(
"fields": [ "fields": [
{ {
"name": f"Wersja `{version_name}`", "name": f"Wersja `{version_name}`",
"value": f"[Pobierz .APK]({download_url})" "value": (
if download_url f"[Pobierz .APK]({download_url})"
else "*Pobieranie niedostępne*", if download_url
else "*Pobieranie niedostępne*"
),
"inline": False, "inline": False,
}, },
{ {
@ -103,16 +102,14 @@ if __name__ == "__main__":
load_dotenv() load_dotenv()
APK_FILE = os.getenv("APK_FILE") APK_FILE = os.getenv("APK_FILE")
APK_SERVER_RELEASE = os.getenv("APK_SERVER_RELEASE") DOWNLOAD_URL = os.getenv("DOWNLOAD_URL")
APK_SERVER_NIGHTLY = os.getenv("APK_SERVER_NIGHTLY")
WEBHOOK_RELEASE = os.getenv("WEBHOOK_RELEASE") WEBHOOK_RELEASE = os.getenv("WEBHOOK_RELEASE")
WEBHOOK_TESTING = os.getenv("WEBHOOK_TESTING") WEBHOOK_TESTING = os.getenv("WEBHOOK_TESTING")
post_webhook( post_webhook(
project_dir, project_dir,
APK_FILE, APK_FILE,
APK_SERVER_RELEASE, DOWNLOAD_URL,
APK_SERVER_NIGHTLY,
WEBHOOK_RELEASE, WEBHOOK_RELEASE,
WEBHOOK_TESTING, WEBHOOK_TESTING,
) )

195
.github/workflows/_build.yml vendored Normal file
View File

@ -0,0 +1,195 @@
name: "[reusable] Szkolny.eu Build"
on:
workflow_call:
inputs:
nightly:
type: boolean
default: false
build-apk:
type: boolean
default: false
build-aab:
type: boolean
default: false
release-ssh:
type: boolean
default: false
release-github:
type: boolean
default: false
release-firebase:
type: boolean
default: false
release-google-play:
type: boolean
default: false
release-discord:
type: boolean
default: false
secrets:
APK_SERVER_NIGHTLY:
APK_SERVER_RELEASE:
DB_HOST:
DB_NAME:
DB_PASS:
DB_USER:
FIREBASE_APP_ID:
FIREBASE_GROUPS_NIGHTLY:
FIREBASE_GROUPS_RELEASE:
FIREBASE_SERVICE_ACCOUNT_JSON:
KEY_ALIAS_PASSWORD:
KEY_ALIAS:
KEY_STORE_PASSWORD:
KEY_STORE:
PLAY_RELEASE_TRACK:
PLAY_SERVICE_ACCOUNT_JSON:
SSH_IP:
SSH_KEY:
SSH_PATH_NIGHTLY:
SSH_PATH_RELEASE:
SSH_USERNAME:
WEBHOOK_RELEASE:
WEBHOOK_TESTING:
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
clean: false
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
distribution: "temurin"
java-version: "17"
- name: Setup Python
uses: actions/setup-python@v4
- name: Install Python packages
uses: BSFishy/pip-action@v1
with:
packages: |
python-dotenv
pycryptodome
mysql-connector-python
requests
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Bump nightly version
if: ${{ inputs.nightly }}
run: python $GITHUB_WORKSPACE/.github/utils/bump_nightly.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT
- name: Write signing passwords and keystore
env:
DB_HOST: ${{ secrets.DB_HOST }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_NAME: ${{ secrets.DB_NAME }}
KEY_STORE: ${{ secrets.KEY_STORE }}
run: |
python $GITHUB_WORKSPACE/.github/utils/sign.py $GITHUB_WORKSPACE commit >> $GITHUB_OUTPUT
echo $KEY_STORE | base64 --decode > keystore.jks
- name: Clean build artifacts
run: |
rm -rf app/release/*
rm -rf app/build/outputs/apk/*
rm -rf app/build/outputs/bundle/*
- name: Build app with Gradle
if: ${{ inputs.build-apk || inputs.build-aab }}
run: |
chmod +x ./gradlew
./gradlew \
${{ inputs.build-apk && 'assembleOfficialRelease' || '' }} \
${{ inputs.build-aab && 'bundlePlayRelease' || '' }} \
-P android.injected.signing.store.file=${{ github.workspace }}/keystore.jks \
-P android.injected.signing.store.password=${{ secrets.KEY_STORE_PASSWORD }} \
-P android.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} \
-P android.injected.signing.key.password=${{ secrets.KEY_ALIAS_PASSWORD }}
- name: Upload release to server
if: ${{ inputs.release-ssh }}
uses: easingthemes/ssh-deploy@v2.1.6
env:
REMOTE_HOST: ${{ secrets.SSH_IP }}
REMOTE_USER: ${{ secrets.SSH_USERNAME }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }}
SOURCE: app/release/
TARGET: ${{ inputs.nightly && secrets.SSH_PATH_NIGHTLY || secrets.SSH_PATH_RELEASE }}
- name: Find signed artifacts
id: artifacts
run: python $GITHUB_WORKSPACE/.github/utils/find_artifacts.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT
- name: Extract release changelogs
id: changelog
run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT
- name: Save version to database
id: save
env:
DB_HOST: ${{ secrets.DB_HOST }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_NAME: ${{ secrets.DB_NAME }}
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
run: python $GITHUB_WORKSPACE/.github/utils/save_version.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT
- name: Release on GitHub
if: ${{ inputs.release-github }}
uses: softprops/action-gh-release@v1
with:
name: ${{ steps.changelog.outputs.changelogTitle }}
body_path: ${{ steps.changelog.outputs.changelogMarkdownFile }}
files: ${{ steps.artifacts.outputs.signedReleaseFile }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Distribute to App Distribution
if: ${{ inputs.release-firebase }}
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }}
file: ${{ steps.artifacts.outputs.signedReleaseFile }}
groups: ${{ inputs.nightly && secrets.FIREBASE_GROUPS_NIGHTLY || secrets.FIREBASE_GROUPS_RELEASE }}
releaseNotesFile: ${{ inputs.nightly && steps.changelog.outputs.commitLogPlainFile || steps.changelog.outputs.changelogPlainTitledFile }}
- name: Publish AAB to Google Play
if: ${{ inputs.release-google-play }}
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
packageName: pl.szczodrzynski.edziennik
releaseFiles: ${{ steps.artifacts.outputs.signedReleaseFile }}
releaseName: ${{ steps.changelog.outputs.appVersionName }}
track: ${{ secrets.PLAY_RELEASE_TRACK }}
whatsNewDirectory: ${{ steps.changelog.outputs.changelogDir }}
status: completed
- name: Post Discord webhook
if: ${{ inputs.release-discord }}
env:
APK_FILE: ${{ steps.artifacts.outputs.signedReleaseFile }}
DOWNLOAD_URL: ${{ steps.save.outputs.downloadUrl }}
WEBHOOK_RELEASE: ${{ secrets.WEBHOOK_RELEASE }}
WEBHOOK_TESTING: ${{ secrets.WEBHOOK_TESTING }}
run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT
- name: Upload workflow artifact
uses: actions/upload-artifact@v2
if: always()
with:
name: ${{ steps.changelog.outputs.appVersionName }}
path: |
app/release/whatsnew*/
app/release/*.apk
app/release/*.aab
app/release/*.json
app/release/*.txt

View File

@ -1,154 +0,0 @@
name: Nightly build
on:
schedule:
# 23:30 UTC, 0:30 or 1:30 CET/CEST
- cron: "30 23 * * *"
workflow_dispatch:
jobs:
prepare:
name: Prepare build environment
runs-on: self-hosted
outputs:
hasNewChanges: ${{ steps.nightly.outputs.hasNewChanges }}
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0
clean: false
- name: Set executable permissions to gradlew
run: chmod +x ./gradlew
- name: Setup Python
uses: actions/setup-python@v2
- name: Install packages
uses: BSFishy/pip-action@v1
with:
packages: |
python-dotenv
pycryptodome
mysql-connector-python
requests
- name: Bump nightly version
id: nightly
run: python $GITHUB_WORKSPACE/.github/utils/bump_nightly.py $GITHUB_WORKSPACE
- name: Write signing passwords
if: steps.nightly.outputs.hasNewChanges
env:
DB_HOST: ${{ secrets.DB_HOST }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_NAME: ${{ secrets.DB_NAME }}
run: python $GITHUB_WORKSPACE/.github/utils/sign.py $GITHUB_WORKSPACE commit
build:
name: Build APK
runs-on: self-hosted
needs:
- prepare
if: ${{ needs.prepare.outputs.hasNewChanges == 'true' }}
outputs:
androidHome: ${{ env.ANDROID_HOME }}
androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }}
steps:
- name: Setup JDK 11
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: '11'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Clean build artifacts
run: |
rm -rf app/release/*
rm -rf app/build/outputs/apk/*
rm -rf app/build/outputs/bundle/*
- name: Assemble official release with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: assembleOfficialRelease
sign:
name: Sign APK
runs-on: self-hosted
needs:
- build
outputs:
signedReleaseFile: ${{ steps.artifacts.outputs.signedReleaseFile }}
signedReleaseFileRelative: ${{ steps.artifacts.outputs.signedReleaseFileRelative }}
steps:
- name: Sign build artifacts
id: sign_app
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/release
signingKeyBase64: ${{ secrets.KEY_STORE }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_ALIAS_PASSWORD }}
env:
ANDROID_HOME: ${{ needs.build.outputs.androidHome }}
ANDROID_SDK_ROOT: ${{ needs.build.outputs.androidSdkRoot }}
BUILD_TOOLS_VERSION: "30.0.2"
- name: Rename signed artifacts
id: artifacts
run: python $GITHUB_WORKSPACE/.github/utils/rename_artifacts.py $GITHUB_WORKSPACE
publish:
name: Publish APK
runs-on: self-hosted
needs:
- sign
steps:
- name: Setup Python
uses: actions/setup-python@v2
- name: Extract changelogs
id: changelog
run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE
- name: Upload APK to SFTP
uses: easingthemes/ssh-deploy@v2.1.6
env:
REMOTE_HOST: ${{ secrets.SSH_IP }}
REMOTE_USER: ${{ secrets.SSH_USERNAME }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }}
SOURCE: ${{ needs.sign.outputs.signedReleaseFileRelative }}
TARGET: ${{ secrets.SSH_PATH_NIGHTLY }}
- name: Save version metadata
env:
DB_HOST: ${{ secrets.DB_HOST }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_NAME: ${{ secrets.DB_NAME }}
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
run: python $GITHUB_WORKSPACE/.github/utils/save_version.py $GITHUB_WORKSPACE
- name: Distribute to App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
token: ${{ secrets.FIREBASE_TOKEN }}
groups: ${{ secrets.FIREBASE_GROUPS_NIGHTLY }}
file: ${{ needs.sign.outputs.signedReleaseFile }}
releaseNotesFile: ${{ steps.changelog.outputs.commitLogPlainFile }}
- name: Post Discord webhook
env:
APK_FILE: ${{ needs.sign.outputs.signedReleaseFile }}
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
WEBHOOK_RELEASE: ${{ secrets.WEBHOOK_RELEASE }}
WEBHOOK_TESTING: ${{ secrets.WEBHOOK_TESTING }}
run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE
- name: Upload workflow artifact
uses: actions/upload-artifact@v2
if: true
with:
name: ${{ steps.changelog.outputs.appVersionName }}
path: |
app/release/whatsnew*/
app/release/*.apk
app/release/*.aab
app/release/*.json
app/release/*.txt

View File

@ -1,131 +0,0 @@
name: Release build - Google Play [AAB]
on:
push:
branches:
- "master"
jobs:
prepare:
name: Prepare build environment
runs-on: self-hosted
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0
clean: false
- name: Set executable permissions to gradlew
run: chmod +x ./gradlew
- name: Setup Python
uses: actions/setup-python@v2
- name: Install packages
uses: BSFishy/pip-action@v1
with:
packages: |
python-dotenv
pycryptodome
mysql-connector-python
requests
- name: Write signing passwords
env:
DB_HOST: ${{ secrets.DB_HOST }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_NAME: ${{ secrets.DB_NAME }}
run: python $GITHUB_WORKSPACE/.github/utils/sign.py $GITHUB_WORKSPACE commit
build:
name: Build App Bundle
runs-on: self-hosted
needs:
- prepare
outputs:
androidHome: ${{ env.ANDROID_HOME }}
androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }}
steps:
- name: Setup JDK 11
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: '11'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Clean build artifacts
run: |
rm -rf app/release/*
rm -rf app/build/outputs/apk/*
rm -rf app/build/outputs/bundle/*
- name: Bundle play release with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: bundlePlayRelease
sign:
name: Sign App Bundle
runs-on: self-hosted
needs:
- build
outputs:
signedReleaseFile: ${{ steps.artifacts.outputs.signedReleaseFile }}
signedReleaseFileRelative: ${{ steps.artifacts.outputs.signedReleaseFileRelative }}
steps:
- name: Sign build artifacts
id: sign_app
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/release
signingKeyBase64: ${{ secrets.KEY_STORE }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_ALIAS_PASSWORD }}
env:
ANDROID_HOME: ${{ needs.build.outputs.androidHome }}
ANDROID_SDK_ROOT: ${{ needs.build.outputs.androidSdkRoot }}
BUILD_TOOLS_VERSION: "30.0.2"
- name: Rename signed artifacts
id: artifacts
run: python $GITHUB_WORKSPACE/.github/utils/rename_artifacts.py $GITHUB_WORKSPACE
publish:
name: Publish App Bundle
runs-on: self-hosted
needs:
- sign
steps:
- name: Setup Python
uses: actions/setup-python@v2
- name: Extract changelogs
id: changelog
run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE
- name: Save version metadata
env:
DB_HOST: ${{ secrets.DB_HOST }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_NAME: ${{ secrets.DB_NAME }}
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
run: python $GITHUB_WORKSPACE/.github/utils/save_version.py $GITHUB_WORKSPACE
- name: Publish AAB to Google Play
uses: r0adkll/upload-google-play@v1
if: ${{ endsWith(needs.sign.outputs.signedReleaseFile, '.aab') }}
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
packageName: pl.szczodrzynski.edziennik
releaseFile: ${{ needs.sign.outputs.signedReleaseFile }}
releaseName: ${{ steps.changelog.outputs.appVersionName }}
track: ${{ secrets.PLAY_RELEASE_TRACK }}
whatsNewDirectory: ${{ steps.changelog.outputs.changelogDir }}
- name: Upload workflow artifact
uses: actions/upload-artifact@v2
if: always()
with:
name: ${{ steps.changelog.outputs.appVersionName }}
path: |
app/release/whatsnew*/
app/release/*.apk
app/release/*.aab
app/release/*.json
app/release/*.txt

View File

@ -1,154 +0,0 @@
name: Release build - official
on:
push:
tags:
- "*"
jobs:
prepare:
name: Prepare build environment
runs-on: self-hosted
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0
clean: false
- name: Set executable permissions to gradlew
run: chmod +x ./gradlew
- name: Setup Python
uses: actions/setup-python@v2
- name: Install packages
uses: BSFishy/pip-action@v1
with:
packages: |
python-dotenv
pycryptodome
mysql-connector-python
requests
- name: Write signing passwords
env:
DB_HOST: ${{ secrets.DB_HOST }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_NAME: ${{ secrets.DB_NAME }}
run: python $GITHUB_WORKSPACE/.github/utils/sign.py $GITHUB_WORKSPACE commit
build:
name: Build APK
runs-on: self-hosted
needs:
- prepare
outputs:
androidHome: ${{ env.ANDROID_HOME }}
androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }}
steps:
- name: Setup JDK 11
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: '11'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Clean build artifacts
run: |
rm -rf app/release/*
rm -rf app/build/outputs/apk/*
rm -rf app/build/outputs/bundle/*
- name: Assemble official release with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: assembleOfficialRelease
sign:
name: Sign APK
runs-on: self-hosted
needs:
- build
outputs:
signedReleaseFile: ${{ steps.artifacts.outputs.signedReleaseFile }}
signedReleaseFileRelative: ${{ steps.artifacts.outputs.signedReleaseFileRelative }}
steps:
- name: Sign build artifacts
id: sign_app
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/release
signingKeyBase64: ${{ secrets.KEY_STORE }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_ALIAS_PASSWORD }}
env:
ANDROID_HOME: ${{ needs.build.outputs.androidHome }}
ANDROID_SDK_ROOT: ${{ needs.build.outputs.androidSdkRoot }}
BUILD_TOOLS_VERSION: "30.0.2"
- name: Rename signed artifacts
id: artifacts
run: python $GITHUB_WORKSPACE/.github/utils/rename_artifacts.py $GITHUB_WORKSPACE
publish:
name: Publish APK
runs-on: self-hosted
needs:
- sign
steps:
- name: Setup Python
uses: actions/setup-python@v2
- name: Extract changelogs
id: changelog
run: python $GITHUB_WORKSPACE/.github/utils/extract_changelogs.py $GITHUB_WORKSPACE
- name: Upload APK to SFTP
uses: easingthemes/ssh-deploy@v2.1.6
env:
REMOTE_HOST: ${{ secrets.SSH_IP }}
REMOTE_USER: ${{ secrets.SSH_USERNAME }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }}
SOURCE: ${{ needs.sign.outputs.signedReleaseFileRelative }}
TARGET: ${{ secrets.SSH_PATH_RELEASE }}
- name: Save version metadata
env:
DB_HOST: ${{ secrets.DB_HOST }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASS: ${{ secrets.DB_PASS }}
DB_NAME: ${{ secrets.DB_NAME }}
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
run: python $GITHUB_WORKSPACE/.github/utils/save_version.py $GITHUB_WORKSPACE
- name: Distribute to App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
token: ${{ secrets.FIREBASE_TOKEN }}
groups: ${{ secrets.FIREBASE_GROUPS_RELEASE }}
file: ${{ needs.sign.outputs.signedReleaseFile }}
releaseNotesFile: ${{ steps.changelog.outputs.changelogPlainTitledFile }}
- name: Release on GitHub
uses: softprops/action-gh-release@v1
with:
name: ${{ steps.changelog.outputs.changelogTitle }}
body_path: ${{ steps.changelog.outputs.changelogMarkdownFile }}
files: ${{ needs.sign.outputs.signedReleaseFile }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Post Discord webhook
env:
APK_FILE: ${{ needs.sign.outputs.signedReleaseFile }}
APK_SERVER_RELEASE: ${{ secrets.APK_SERVER_RELEASE }}
APK_SERVER_NIGHTLY: ${{ secrets.APK_SERVER_NIGHTLY }}
WEBHOOK_RELEASE: ${{ secrets.WEBHOOK_RELEASE }}
WEBHOOK_TESTING: ${{ secrets.WEBHOOK_TESTING }}
run: python $GITHUB_WORKSPACE/.github/utils/webhook_discord.py $GITHUB_WORKSPACE
- name: Upload workflow artifact
uses: actions/upload-artifact@v2
if: true
with:
name: ${{ steps.changelog.outputs.appVersionName }}
path: |
app/release/whatsnew*/
app/release/*.apk
app/release/*.aab
app/release/*.json
app/release/*.txt

13
.github/workflows/push-master.yml vendored Normal file
View File

@ -0,0 +1,13 @@
name: Push (master)
on:
push:
branches: ["master"]
jobs:
build:
name: Build for Google Play (AAB)
uses: szkolny-eu/szkolny-android/.github/workflows/_build.yml@develop
with:
build-aab: true
release-ssh: true
release-google-play: true
secrets: inherit

15
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,15 @@
name: Release
on:
push:
tags: ["v*.*.*"]
jobs:
build:
name: Build release (APK)
uses: szkolny-eu/szkolny-android/.github/workflows/_build.yml@develop
with:
build-apk: true
release-ssh: true
release-github: true
release-firebase: true
release-discord: true
secrets: inherit

42
.github/workflows/schedule-dispatch.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Schedule/dispatch
on:
schedule:
# 23:30 UTC, 0:30 or 1:30 CET/CEST
- cron: "30 23 * * *"
workflow_dispatch:
jobs:
check:
name: Check new changes
runs-on: ubuntu-latest
outputs:
hasNewChanges: ${{ steps.nightly.outputs.hasNewChanges }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
clean: false
- name: Setup Python
uses: actions/setup-python@v4
- name: Install packages
uses: BSFishy/pip-action@v1
with:
packages: |
requests
- name: Check new changes
id: nightly
run: python $GITHUB_WORKSPACE/.github/utils/check_nightly.py $GITHUB_WORKSPACE >> $GITHUB_OUTPUT
build:
name: Build nightly release (APK)
needs:
- check
if: ${{ needs.check.outputs.hasNewChanges == 'true' }}
uses: szkolny-eu/szkolny-android/.github/workflows/_build.yml@develop
with:
nightly: true
build-apk: true
release-ssh: true
release-firebase: true
release-discord: true
secrets: inherit

View File

@ -1,9 +1,17 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="ALLOW_TRAILING_COMMA" value="true" /> <option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="USE_TAB_CHARACTER" value="true" />
<option name="SMART_TABS" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML"> <codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" /> <option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions> <indentOptions>

View File

@ -5,6 +5,7 @@
<w>ciasteczko</w> <w>ciasteczko</w>
<w>csrf</w> <w>csrf</w>
<w>edziennik</w> <w>edziennik</w>
<w>eggfall</w>
<w>elearning</w> <w>elearning</w>
<w>gson</w> <w>gson</w>
<w>hebe</w> <w>hebe</w>

View File

@ -104,7 +104,6 @@ android {
externalNativeBuild { externalNativeBuild {
cmake { cmake {
path "src/main/cpp/CMakeLists.txt" path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
} }
} }
lint { lint {
@ -113,8 +112,9 @@ android {
} }
tasks.whenTaskAdded { task -> tasks.whenTaskAdded { task ->
if (!task.name.endsWith("Release") && !task.name.endsWith("ReleaseWithR8")) if (!(task.name == "assembleUnofficialRelease" || task.name == "assembleOfficialRelease" || task.name == "signPlayReleaseBundle"))
return return
def renameTaskName = "rename${task.name.capitalize()}" def renameTaskName = "rename${task.name.capitalize()}"
def flavor = "" def flavor = ""
@ -124,17 +124,22 @@ tasks.whenTaskAdded { task ->
flavor = task.name.substring("assemble".length(), task.name.indexOf("Release")).uncapitalize() flavor = task.name.substring("assemble".length(), task.name.indexOf("Release")).uncapitalize()
if (task.name.startsWith("minify")) if (task.name.startsWith("minify"))
flavor = task.name.substring("minify".length(), task.name.indexOf("Release")).uncapitalize() flavor = task.name.substring("minify".length(), task.name.indexOf("Release")).uncapitalize()
if (task.name.startsWith("sign"))
flavor = task.name.substring("sign".length(), task.name.indexOf("Release")).uncapitalize()
if (flavor != "") { if (flavor != "") {
tasks.create(renameTaskName, Copy) { tasks.register(renameTaskName, Copy) {
dependsOn(task.name)
duplicatesStrategy DuplicatesStrategy.FAIL
from file("${projectDir}/${flavor}/release/"), from file("${projectDir}/${flavor}/release/"),
file("${buildDir}/outputs/mapping/${flavor}Release/"), file("${projectDir}/build/outputs/apk/${flavor}/release/"),
file("${buildDir}/outputs/apk/${flavor}/release/"), file("${projectDir}/build/outputs/mapping/${flavor}Release/"),
file("${buildDir}/outputs/bundle/${flavor}Release/") file("${projectDir}/build/outputs/bundle/${flavor}Release/")
include "*.aab", "*.apk", "mapping.txt", "output-metadata.json" include "*-release.aab", "*-release.apk", "mapping.txt", "output-metadata.json"
destinationDir file("${projectDir}/release/") destinationDir file("${projectDir}/release/")
rename ".+?\\.(.+)", "Edziennik_${android.defaultConfig.versionName}_${flavor}." + '$1' rename ".+?\\.(.+)", "Edziennik_${android.defaultConfig.versionName}_${flavor}." + '$1'
} }
task.finalizedBy(renameTaskName) task.finalizedBy(renameTaskName)
} }
} }
@ -156,6 +161,7 @@ dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:2.5.2" implementation "androidx.navigation:navigation-fragment-ktx:2.5.2"
implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.room:room-runtime:2.4.3" implementation "androidx.room:room-runtime:2.4.3"
implementation "androidx.room:room-ktx:2.4.3"
implementation "androidx.work:work-runtime-ktx:2.7.1" implementation "androidx.work:work-runtime-ktx:2.7.1"
kapt "androidx.room:room-compiler:2.4.3" kapt "androidx.room:room-compiler:2.4.3"
@ -208,7 +214,7 @@ dependencies {
implementation "com.daimajia.swipelayout:library:1.2.0@aar" implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.github.Applandeo:Material-Calendar-View:15de569cbc" // https://github.com/Applandeo/Material-Calendar-View implementation "com.github.Applandeo:Material-Calendar-View:15de569cbc" // https://github.com/Applandeo/Material-Calendar-View
implementation "com.github.CanHub:Android-Image-Cropper:2.2.2" // https://github.com/CanHub/Android-Image-Cropper implementation "com.github.CanHub:Android-Image-Cropper:2.2.2" // https://github.com/CanHub/Android-Image-Cropper
implementation "com.github.ChuckerTeam.Chucker:library:3.0.1" // https://github.com/ChuckerTeam/chucker implementation "com.github.ChuckerTeam.Chucker:library:3.5.2" // https://github.com/ChuckerTeam/chucker
implementation "com.github.antonKozyriatskyi:CircularProgressIndicator:1.2.2" // https://github.com/antonKozyriatskyi/CircularProgressIndicator implementation "com.github.antonKozyriatskyi:CircularProgressIndicator:1.2.2" // https://github.com/antonKozyriatskyi/CircularProgressIndicator
implementation "com.github.bassaer:chatmessageview:2.0.1" // https://github.com/bassaer/ChatMessageView implementation "com.github.bassaer:chatmessageview:2.0.1" // https://github.com/bassaer/ChatMessageView
implementation "com.github.hypertrack:hyperlog-android:0.0.10" // https://github.com/hypertrack/hyperlog-android implementation "com.github.hypertrack:hyperlog-android:0.0.10" // https://github.com/hypertrack/hyperlog-android

View File

@ -22,6 +22,7 @@
-keep class android.support.v7.widget.** { *; } -keep class android.support.v7.widget.** { *; }
-keep class pl.szczodrzynski.edziennik.utils.models.** { *; } -keep class pl.szczodrzynski.edziennik.utils.models.** { *; }
-keep class pl.szczodrzynski.edziennik.data.db.enums.* { *; }
-keep class pl.szczodrzynski.edziennik.data.db.entity.Event { *; } -keep class pl.szczodrzynski.edziennik.data.db.entity.Event { *; }
-keep class pl.szczodrzynski.edziennik.data.db.full.EventFull { *; } -keep class pl.szczodrzynski.edziennik.data.db.full.EventFull { *; }
-keep class pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage { *; } -keep class pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage { *; }
@ -31,6 +32,9 @@
-keepnames class pl.szczodrzynski.edziennik.ui.widgets.timetable.WidgetTimetableProvider -keepnames class pl.szczodrzynski.edziennik.ui.widgets.timetable.WidgetTimetableProvider
-keepnames class pl.szczodrzynski.edziennik.ui.widgets.notifications.WidgetNotificationsProvider -keepnames class pl.szczodrzynski.edziennik.ui.widgets.notifications.WidgetNotificationsProvider
-keepnames class pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider -keepnames class pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider
-keep class pl.szczodrzynski.edziennik.config.AppData { *; }
-keep class pl.szczodrzynski.edziennik.config.AppData$** { *; }
-keep class pl.szczodrzynski.edziennik.utils.managers.TextStylingManager$HtmlMode { *; }
-keepnames class androidx.appcompat.view.menu.MenuBuilder { setHeaderTitleInt(java.lang.CharSequence); } -keepnames class androidx.appcompat.view.menu.MenuBuilder { setHeaderTitleInt(java.lang.CharSequence); }
-keepnames class androidx.appcompat.view.menu.MenuPopupHelper { showPopup(int, int, boolean, boolean); } -keepnames class androidx.appcompat.view.menu.MenuPopupHelper { showPopup(int, int, boolean, boolean); }

File diff suppressed because it is too large Load Diff

View File

@ -160,7 +160,11 @@
<activity android:name=".ui.login.oauth.OAuthLoginActivity" <activity android:name=".ui.login.oauth.OAuthLoginActivity"
android:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
android:exported="false" android:exported="false"
android:theme="@style/AppTheme.Light" /> android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" />
<activity android:name=".ui.login.recaptcha.RecaptchaActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" />
<activity android:name=".ui.base.BuildInvalidActivity" android:exported="false" /> <activity android:name=".ui.base.BuildInvalidActivity" android:exported="false" />
<activity android:name=".ui.settings.contributors.ContributorsActivity" android:exported="false" /> <activity android:name=".ui.settings.contributors.ContributorsActivity" android:exported="false" />

View File

@ -1,12 +1,8 @@
<h3>Wersja 4.13-beta.3, 2022-10-20</h3> <h3>Wersja 4.13.7, 2024-07-08</h3>
<ul> <ul>
<li>Poprawione powiadomienia na Androidzie 13. @santoni0</li> <li>Dodano opcję uruchomienia aplikacji bez logowania.</li>
<li>Możliwość dostosowania wyświetlania planu lekcji</li>
<li>Opcja kolorowania bloków w planie lekcji</li>
<li><b>USOS</b> - pierwsza wersja obsługi systemu</li>
<li>Poprawione opcje filtrowania powiadomień i wyboru przycisków menu bocznego.</li>
</ul> </ul>
<br> <br>
<br> <br>
Dzięki za korzystanie ze Szkolnego!<br> Dzięki za korzystanie ze Szkolnego!<br>
<i>&copy; [Kuba Szczodrzyński](@kuba2k2), [Kacper Ziubryniewicz](@kapi2289) 2022</i> <i>&copy; [Kuba Szczodrzyński](@kuba2k2) 2023</i>

View File

@ -5,6 +5,8 @@
cmake_minimum_required(VERSION 3.4.1) cmake_minimum_required(VERSION 3.4.1)
project(szkolny-signing)
# Creates and names a library, sets it as either STATIC # Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code. # or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you. # You can define multiple libraries, and CMake builds them for you.
@ -41,4 +43,4 @@ target_link_libraries( # Specifies the target library.
# Links the target library to the log library # Links the target library to the log library
# included in the NDK. # included in the NDK.
${log-lib} ) ${log-lib} )

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/ /*secret password - removed for source code publication*/
static toys AES_IV[16] = { static toys AES_IV[16] = {
0x74, 0xc9, 0x77, 0x6d, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; 0x0e, 0x87, 0x6d, 0xaa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -12,6 +12,7 @@ import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import androidx.work.Configuration import androidx.work.Configuration
@ -27,10 +28,15 @@ import com.google.gson.Gson
import com.hypertrack.hyperlog.HyperLog import com.hypertrack.hyperlog.HyperLog
import com.mikepenz.iconics.Iconics import com.mikepenz.iconics.Iconics
import im.wangchao.mhttp.MHttp import im.wangchao.mhttp.MHttp
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.leolin.shortcutbadger.ShortcutBadger import me.leolin.shortcutbadger.ShortcutBadger
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.config.AppData
import pl.szczodrzynski.edziennik.config.Config import pl.szczodrzynski.edziennik.config.Config
import pl.szczodrzynski.edziennik.data.api.events.ProfileListEmptyEvent import pl.szczodrzynski.edziennik.data.api.events.ProfileListEmptyEvent
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
@ -48,18 +54,39 @@ import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateWorker import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.base.CrashActivity import pl.szczodrzynski.edziennik.ui.base.CrashActivity
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
import pl.szczodrzynski.edziennik.utils.* import pl.szczodrzynski.edziennik.utils.DebugLogFormat
import pl.szczodrzynski.edziennik.utils.PermissionChecker
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.managers.* import pl.szczodrzynski.edziennik.utils.managers.AttendanceManager
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager
import pl.szczodrzynski.edziennik.utils.managers.BuildManager
import pl.szczodrzynski.edziennik.utils.managers.EventManager
import pl.szczodrzynski.edziennik.utils.managers.GradesManager
import pl.szczodrzynski.edziennik.utils.managers.MessageManager
import pl.szczodrzynski.edziennik.utils.managers.NoteManager
import pl.szczodrzynski.edziennik.utils.managers.NotificationChannelsManager
import pl.szczodrzynski.edziennik.utils.managers.PermissionManager
import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager
import pl.szczodrzynski.edziennik.utils.managers.TimetableManager
import pl.szczodrzynski.edziennik.utils.managers.UpdateManager
import pl.szczodrzynski.edziennik.utils.managers.UserActionManager
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.system.exitProcess
class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
companion object { companion object {
@Volatile @Volatile
lateinit var db: AppDb lateinit var db: AppDb
private set
lateinit var config: Config lateinit var config: Config
// private set // for LabFragment
lateinit var profile: Profile lateinit var profile: Profile
private set
lateinit var data: AppData
private set
val profileId val profileId
get() = profile.id get() = profile.id
@ -69,18 +96,19 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
} }
val api by lazy { SzkolnyApi(this) } val api by lazy { SzkolnyApi(this) }
val notificationChannelsManager by lazy { NotificationChannelsManager(this) }
val userActionManager by lazy { UserActionManager(this) }
val gradesManager by lazy { GradesManager(this) }
val timetableManager by lazy { TimetableManager(this) }
val eventManager by lazy { EventManager(this) }
val permissionManager by lazy { PermissionManager(this) }
val attendanceManager by lazy { AttendanceManager(this) } val attendanceManager by lazy { AttendanceManager(this) }
val buildManager by lazy { BuildManager(this) }
val availabilityManager by lazy { AvailabilityManager(this) } val availabilityManager by lazy { AvailabilityManager(this) }
val textStylingManager by lazy { TextStylingManager(this) } val buildManager by lazy { BuildManager(this) }
val eventManager by lazy { EventManager(this) }
val gradesManager by lazy { GradesManager(this) }
val messageManager by lazy { MessageManager(this) } val messageManager by lazy { MessageManager(this) }
val noteManager by lazy { NoteManager(this) } val noteManager by lazy { NoteManager(this) }
val notificationChannelsManager by lazy { NotificationChannelsManager(this) }
val permissionManager by lazy { PermissionManager(this) }
val textStylingManager by lazy { TextStylingManager(this) }
val timetableManager by lazy { TimetableManager(this) }
val updateManager by lazy { UpdateManager(this) }
val userActionManager by lazy { UserActionManager(this) }
val db val db
get() = App.db get() = App.db
@ -90,6 +118,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
get() = App.profile get() = App.profile
val profileId val profileId
get() = App.profileId get() = App.profileId
val data
get() = App.data
private val job = Job() private val job = Job()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
@ -124,9 +154,6 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
SSLProviderInstaller.enableSupportedTls(builder, enableCleartext = true) SSLProviderInstaller.enableSupportedTls(builder, enableCleartext = true)
if (devMode) { if (devMode) {
HyperLog.initialize(this)
HyperLog.setLogLevel(Log.VERBOSE)
HyperLog.setLogFormat(DebugLogFormat(this))
if (enableChucker) { if (enableChucker) {
val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR) val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR)
val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector) val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector)
@ -181,15 +208,23 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
Iconics.respectFontBoundsDefault = true Iconics.respectFontBoundsDefault = true
// initialize companion object values // initialize companion object values
AppData.read(this)
App.db = AppDb(this) App.db = AppDb(this)
App.config = Config(App.db) App.config = Config(App.db)
App.profile = Profile(0, 0, LoginType.TEMPLATE, "")
debugMode = BuildConfig.DEBUG debugMode = BuildConfig.DEBUG
devMode = config.devMode ?: debugMode devMode = config.devMode ?: debugMode
enableChucker = config.enableChucker ?: devMode enableChucker = config.enableChucker ?: devMode
if (devMode) {
HyperLog.initialize(this)
HyperLog.setLogLevel(Log.VERBOSE)
HyperLog.setLogFormat(DebugLogFormat(this))
}
if (!profileLoadById(config.lastProfileId)) { if (!profileLoadById(config.lastProfileId)) {
db.profileDao().firstId?.let { profileLoadById(it) } val success = db.profileDao().firstId?.let { profileLoadById(it) }
if (success != true)
profileLoad(Profile(0, 0, LoginType.TEMPLATE, ""))
} }
buildHttp() buildHttp()
@ -200,6 +235,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
} }
Signing.getCert(this) Signing.getCert(this)
Utils.initializeStorageDir(this)
launch { launch {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
@ -381,10 +417,28 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
} }
} }
fun profileLoad(profile: Profile) {
App.profile = profile
App.config.lastProfileId = profile.id
try {
App.data = AppData.get(profile.loginStoreType)
d("App", "Loaded AppData: ${App.data}")
// apply newly-added config overrides, if not changed by the user yet
for ((key, value) in App.data.configOverrides) {
val config = App.profile.config
if (!config.has(key))
config.set(key, value)
}
} catch (e: Exception) {
Log.e("App", "Cannot load AppData", e)
Toast.makeText(this, R.string.app_cannot_load_data, Toast.LENGTH_LONG).show()
exitProcess(0)
}
}
private fun profileLoadById(profileId: Int): Boolean { private fun profileLoadById(profileId: Int): Boolean {
db.profileDao().getByIdNow(profileId)?.also { db.profileDao().getByIdNow(profileId)?.also {
App.profile = it profileLoad(it)
App.config.lastProfileId = it.id
return true return true
} }
return false return false
@ -415,6 +469,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
} }
fun profileSave() = profileSave(profile) fun profileSave() = profileSave(profile)
fun profileSave(profile: Profile) { fun profileSave(profile: Profile) {
if (profile.id == profileId)
App.profile = profile
launch(Dispatchers.Default) { launch(Dispatchers.Default) {
App.db.profileDao().add(profile) App.db.profileDao().add(profile)
} }

View File

@ -42,11 +42,11 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Metadata.* import pl.szczodrzynski.edziennik.data.db.entity.Metadata.*
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
import pl.szczodrzynski.edziennik.data.db.enums.LoginType
import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding
import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent
import pl.szczodrzynski.edziennik.sync.SyncWorker import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateStateEvent
import pl.szczodrzynski.edziennik.sync.UpdateWorker import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.base.MainSnackbar import pl.szczodrzynski.edziennik.ui.base.MainSnackbar
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
@ -57,6 +57,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegisterUnavailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.ServerMessageDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.ServerMessageDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateAvailableDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateAvailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateProgressDialog
import pl.szczodrzynski.edziennik.ui.error.ErrorDetailsDialog import pl.szczodrzynski.edziennik.ui.error.ErrorDetailsDialog
import pl.szczodrzynski.edziennik.ui.error.ErrorSnackbar import pl.szczodrzynski.edziennik.ui.error.ErrorSnackbar
import pl.szczodrzynski.edziennik.ui.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.event.EventManualDialog
@ -321,7 +322,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
// IT'S WINTER MY DUDES // IT'S WINTER MY DUDES
val today = Date.getToday() val today = Date.getToday()
if ((today.month % 11 == 1) && app.config.ui.snowfall) { if ((today.month / 3 % 4 == 0) && app.config.ui.snowfall) {
b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false)) b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false))
} else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) { } else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) {
val eggfall = layoutInflater.inflate( val eggfall = layoutInflater.inflate(
@ -427,7 +428,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
launch { launch {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
app.db.profileDao().allNow.forEach { profile -> app.db.profileDao().allNow.forEach { profile ->
if (profile.loginStoreType != LoginType.LIBRUS) if (!profile.getAppData().uiConfig.enableMarkAsReadAnnouncements)
app.db.metadataDao() app.db.metadataDao()
.setAllSeenExceptMessagesAndAnnouncements(profile.id, true) .setAllSeenExceptMessagesAndAnnouncements(profile.id, true)
else else
@ -537,6 +538,14 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
UpdateAvailableDialog(this, event).show() UpdateAvailableDialog(this, event).show()
} }
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onUpdateStateEvent(event: UpdateStateEvent) {
if (!event.running)
return
EventBus.getDefault().removeStickyEvent(event)
UpdateProgressDialog(this, event.update ?: return, event.downloadId).show()
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true) @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) { fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) {
EventBus.getDefault().removeStickyEvent(event) EventBus.getDefault().removeStickyEvent(event)
@ -695,11 +704,15 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
} }
d(TAG, "}") d(TAG, "}")
val intentProfileId = extras.getIntOrNull("profileId") val intentProfileId = extras.getIntOrNull("profileId").takePositive()
var intentNavTarget = extras.getIntOrNull("fragmentId").asNavTargetOrNull() var intentNavTarget = extras.getIntOrNull("fragmentId").asNavTargetOrNull()
if (extras?.containsKey("action") == true) { if (extras?.containsKey("action") == true) {
val handled = when (extras.getString("action")) { val handled = when (extras.getString("action")) {
"updateRequest" -> {
UpdateAvailableDialog(this, app.config.update).show()
true
}
"serverMessage" -> { "serverMessage" -> {
ServerMessageDialog( ServerMessageDialog(
this, this,
@ -743,8 +756,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
} }
if (extras?.containsKey("reloadProfileId") == true) { if (extras?.containsKey("reloadProfileId") == true) {
val reloadProfileId = extras.getIntOrNull("reloadProfileId") val reloadProfileId = extras.getIntOrNull("reloadProfileId").takePositive()
if (reloadProfileId == -1 || app.profile.id == reloadProfileId) { if (reloadProfileId == null || app.profile.id == reloadProfileId) {
reloadTarget() reloadTarget()
return return
} }
@ -767,7 +780,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
navTarget = intentNavTarget, navTarget = intentNavTarget,
args = extras, args = extras,
) )
intentProfileId != -1 -> navigate( intentProfileId != null -> navigate(
profileId = intentProfileId, profileId = intentProfileId,
navTarget = intentNavTarget, navTarget = intentNavTarget,
args = extras, args = extras,
@ -776,6 +789,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
navTarget = intentNavTarget, navTarget = intentNavTarget,
args = extras, args = extras,
) )
navLoading -> navigate()
else -> drawer.currentProfile = app.profile.id else -> drawer.currentProfile = app.profile.id
} }
navLoading = false navLoading = false
@ -923,7 +937,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
} }
if (profileChanged) { if (profileChanged) {
App.profile = profile if (App.profileId != profile.id)
app.profileLoad(profile)
MessagesFragment.pageSelection = -1 MessagesFragment.pageSelection = -1
// set new drawer items for this profile // set new drawer items for this profile
setDrawerItems() setDrawerItems()

View File

@ -1,9 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-27.
*/
package pl.szczodrzynski.edziennik.config
interface AbstractConfig {
fun set(key: String, value: String?)
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-21.
*/
package pl.szczodrzynski.edziennik.config
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.enums.LoginType
import pl.szczodrzynski.edziennik.ext.getJsonObject
import pl.szczodrzynski.edziennik.ext.mergeWith
import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode
data class AppData(
val configOverrides: Map<String, String>,
val messagesConfig: MessagesConfig,
val uiConfig: UIConfig,
val eventTypes: List<EventType>,
) {
companion object {
private var data: JsonObject? = null
private val appData = mutableMapOf<LoginType, AppData>()
fun read(app: App) {
val res = app.resources.openRawResource(R.raw.app_data)
data = JsonParser.parseReader(JsonReader(res.reader())).asJsonObject
}
fun get(loginType: LoginType): AppData {
if (loginType in appData)
return appData.getValue(loginType)
val json = data?.getJsonObject("base")?.deepCopy()
?: throw NoSuchElementException("Base data not found")
val overrides = setOf(loginType, loginType.schoolType)
for (overrideType in overrides) {
val override = data?.getJsonObject(overrideType.name.lowercase()) ?: continue
json.mergeWith(override)
}
val value = Gson().fromJson(json, AppData::class.java)
appData[loginType] = value
return value
}
}
data class MessagesConfig(
val subjectLength: Int?,
val bodyLength: Int?,
val textStyling: Boolean,
val syncRecipientList: Boolean,
val htmlMode: HtmlMode,
val needsReadStatus: Boolean,
)
data class UIConfig(
val lessonHeight: Int,
val enableMarkAsReadAnnouncements: Boolean,
val enableNoticePoints: Boolean,
val eventManualShowSubjectDropdown: Boolean,
)
data class EventType(
val id: Long,
val color: String,
val name: String,
)
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-27.
*/
package pl.szczodrzynski.edziennik.config
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.config.db.ConfigEntry
import pl.szczodrzynski.edziennik.data.db.AppDb
import pl.szczodrzynski.edziennik.ext.takePositive
import kotlin.coroutines.CoroutineContext
abstract class BaseConfig(
@Transient
val db: AppDb,
val profileId: Int? = null,
protected var entries: List<ConfigEntry>? = null,
) : CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Default
val values = hashMapOf<String, String?>()
init {
if (entries == null)
entries = db.configDao().getAllNow()
values.clear()
for ((profileId, key, value) in entries!!) {
if (profileId.takePositive() != this.profileId)
continue
values[key] = value
}
}
fun set(key: String, value: String?) {
values[key] = value
launch(Dispatchers.IO) {
db.configDao().add(ConfigEntry(profileId ?: -1, key, value))
}
}
fun has(key: String) = values.containsKey(key)
}

View File

@ -5,151 +5,59 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import com.google.gson.JsonObject import com.google.gson.JsonObject
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.App
import pl.szczodrzynski.edziennik.BuildConfig import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.config.db.ConfigEntry
import pl.szczodrzynski.edziennik.config.utils.* import pl.szczodrzynski.edziennik.config.utils.*
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.AppDb
import kotlin.coroutines.CoroutineContext
class Config(val db: AppDb) : CoroutineScope, AbstractConfig { @Suppress("RemoveExplicitTypeArguments")
class Config(db: AppDb) : BaseConfig(db) {
companion object { companion object {
const val DATA_VERSION = 12 const val DATA_VERSION = 12
} }
private val job = Job() private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Default
val values: HashMap<String, String?> = hashMapOf()
val ui by lazy { ConfigUI(this) } val ui by lazy { ConfigUI(this) }
val sync by lazy { ConfigSync(this) } val sync by lazy { ConfigSync(this) }
val timetable by lazy { ConfigTimetable(this) } val timetable by lazy { ConfigTimetable(this) }
val grades by lazy { ConfigGrades(this) } val grades by lazy { ConfigGrades(this) }
private var mDataVersion: Int? = null var dataVersion by config<Int>(DATA_VERSION)
var dataVersion: Int var hash by config<String>("")
get() { mDataVersion = mDataVersion ?: values.get("dataVersion", 0); return mDataVersion ?: 0 }
set(value) { set("dataVersion", value); mDataVersion = value }
private var mHash: String? = null var lastProfileId by config<Int>(0)
var hash: String var loginFinished by config<Boolean>(false)
get() { mHash = mHash ?: values.get("hash", ""); return mHash ?: "" } var privacyPolicyAccepted by config<Boolean>(false)
set(value) { set("hash", value); mHash = value } var update by config<Update?>(null)
var updatesChannel by config<String>("release")
private var mLastProfileId: Int? = null var devMode by config<Boolean?>("debugMode", null)
var lastProfileId: Int var devModePassword by config<String?>(null)
get() { mLastProfileId = mLastProfileId ?: values.get("lastProfileId", 0); return mLastProfileId ?: 0 } var enableChucker by config<Boolean?>(null)
set(value) { set("lastProfileId", value); mLastProfileId = value }
private var mUpdatesChannel: String? = null var apiAvailabilityCheck by config<Boolean>(true)
var updatesChannel: String var apiInvalidCert by config<String?>(null)
get() { mUpdatesChannel = mUpdatesChannel ?: values.get("updatesChannel", "release"); return mUpdatesChannel ?: "release" } var apiKeyCustom by config<String?>(null)
set(value) { set("updatesChannel", value); mUpdatesChannel = value } var appInstalledTime by config<Long>(0L)
private var mUpdate: Update? = null var appRateSnackbarTime by config<Long>(0L)
var update: Update? var appVersion by config<Int>(BuildConfig.VERSION_CODE)
get() { mUpdate = mUpdate ?: values.get("update", null as Update?); return mUpdate ?: null as Update? } var validation by config<String?>(null, "buildValidation")
set(value) { set("update", value); mUpdate = value }
private var mAppVersion: Int? = null var archiverEnabled by config<Boolean>(true)
var appVersion: Int var runSync by config<Boolean>(false)
get() { mAppVersion = mAppVersion ?: values.get("appVersion", BuildConfig.VERSION_CODE); return mAppVersion ?: BuildConfig.VERSION_CODE } var widgetConfigs by config<JsonObject> { JsonObject() }
set(value) { set("appVersion", value); mAppVersion = value }
private var mLoginFinished: Boolean? = null
var loginFinished: Boolean
get() { mLoginFinished = mLoginFinished ?: values.get("loginFinished", false); return mLoginFinished ?: false }
set(value) { set("loginFinished", value); mLoginFinished = value }
private var mPrivacyPolicyAccepted: Boolean? = null
var privacyPolicyAccepted: Boolean
get() { mPrivacyPolicyAccepted = mPrivacyPolicyAccepted ?: values.get("privacyPolicyAccepted", false); return mPrivacyPolicyAccepted ?: false }
set(value) { set("privacyPolicyAccepted", value); mPrivacyPolicyAccepted = value }
private var mDevMode: Boolean? = null
var devMode: Boolean?
get() { mDevMode = mDevMode ?: values.getBooleanOrNull("debugMode"); return mDevMode }
set(value) { set("debugMode", value?.toString()); mDevMode = value }
private var mEnableChucker: Boolean? = null
var enableChucker: Boolean?
get() { mEnableChucker = mEnableChucker ?: values.getBooleanOrNull("enableChucker"); return mEnableChucker }
set(value) { set("enableChucker", value?.toString()); mEnableChucker = value }
private var mDevModePassword: String? = null
var devModePassword: String?
get() { mDevModePassword = mDevModePassword ?: values.get("devModePassword", null as String?); return mDevModePassword }
set(value) { set("devModePassword", value); mDevModePassword = value }
private var mAppInstalledTime: Long? = null
var appInstalledTime: Long
get() { mAppInstalledTime = mAppInstalledTime ?: values.get("appInstalledTime", 0L); return mAppInstalledTime ?: 0L }
set(value) { set("appInstalledTime", value); mAppInstalledTime = value }
private var mAppRateSnackbarTime: Long? = null
var appRateSnackbarTime: Long
get() { mAppRateSnackbarTime = mAppRateSnackbarTime ?: values.get("appRateSnackbarTime", 0L); return mAppRateSnackbarTime ?: 0L }
set(value) { set("appRateSnackbarTime", value); mAppRateSnackbarTime = value }
private var mRunSync: Boolean? = null
var runSync: Boolean
get() { mRunSync = mRunSync ?: values.get("runSync", false); return mRunSync ?: false }
set(value) { set("runSync", value); mRunSync = value }
private var mWidgetConfigs: JsonObject? = null
var widgetConfigs: JsonObject
get() { mWidgetConfigs = mWidgetConfigs ?: values.get("widgetConfigs", JsonObject()); return mWidgetConfigs ?: JsonObject() }
set(value) { set("widgetConfigs", value); mWidgetConfigs = value }
private var mArchiverEnabled: Boolean? = null
var archiverEnabled: Boolean
get() { mArchiverEnabled = mArchiverEnabled ?: values.get("archiverEnabled", true); return mArchiverEnabled ?: true }
set(value) { set("archiverEnabled", value); mArchiverEnabled = value }
private var mValidation: String? = null
var validation: String?
get() { mValidation = mValidation ?: values["buildValidation"]; return mValidation }
set(value) { set("buildValidation", value); mValidation = value }
private var mApiInvalidCert: String? = null
var apiInvalidCert: String?
get() { mApiInvalidCert = mApiInvalidCert ?: values["apiInvalidCert"]; return mApiInvalidCert }
set(value) { set("apiInvalidCert", value); mApiInvalidCert = value }
private var mApiAvailabilityCheck: Boolean? = null
var apiAvailabilityCheck: Boolean
get() { mApiAvailabilityCheck = mApiAvailabilityCheck ?: values.get("apiAvailabilityCheck", true); return mApiAvailabilityCheck ?: true }
set(value) { set("apiAvailabilityCheck", value); mApiAvailabilityCheck = value }
private var rawEntries: List<ConfigEntry> = db.configDao().getAllNow()
private val profileConfigs: HashMap<Int, ProfileConfig> = hashMapOf()
init {
rawEntries.toHashMap(-1, values)
}
fun migrate(app: App) { fun migrate(app: App) {
if (dataVersion < DATA_VERSION) if (dataVersion < DATA_VERSION || hash == "")
// migrate old data version OR freshly installed app (or updated from 3.x)
ConfigMigration(app, this) ConfigMigration(app, this)
} }
fun getFor(profileId: Int): ProfileConfig {
return profileConfigs[profileId] ?: ProfileConfig(db, profileId, db.configDao().getAllNow(profileId)).also { operator fun get(profileId: Int): ProfileConfig {
return profileConfigs[profileId] ?: ProfileConfig(db, profileId, entries).also {
profileConfigs[profileId] = it profileConfigs[profileId] = it
} }
} }
fun forProfile() = getFor(App.profileId)
fun setProfile(profileId: Int) {
}
override fun set(key: String, value: String?) {
values[key] = value
launch {
db.configDao().add(ConfigEntry(-1, key, value))
}
}
} }

View File

@ -4,13 +4,10 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_DATE_DESC
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.utils.managers.GradesManager
class ConfigGrades(private val config: Config) { @Suppress("RemoveExplicitTypeArguments")
private var mOrderBy: Int? = null class ConfigGrades(base: Config) {
var orderBy: Int
get() { mOrderBy = mOrderBy ?: config.values.get("gradesOrderBy", 0); return mOrderBy ?: GradesManager.ORDER_BY_DATE_DESC } var orderBy by base.config<Int>("gradesOrderBy", ORDER_BY_DATE_DESC)
set(value) { config.set("gradesOrderBy", value); mOrderBy = value }
} }

View File

@ -4,139 +4,53 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import pl.szczodrzynski.edziennik.BuildConfig import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.getIntList
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.config.utils.setMap
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.ext.HOUR
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
class ConfigSync(private val config: Config) { @Suppress("RemoveExplicitTypeArguments")
private val gson = Gson() class ConfigSync(base: Config) {
private var mDontShowAppManagerDialog: Boolean? = null var enabled by base.config<Boolean>("syncEnabled", true)
var dontShowAppManagerDialog: Boolean var interval by base.config<Int>("syncInterval", 1 * HOUR.toInt())
get() { mDontShowAppManagerDialog = mDontShowAppManagerDialog ?: config.values.get("dontShowAppManagerDialog", false); return mDontShowAppManagerDialog ?: false } var onlyWifi by base.config<Boolean>("syncOnlyWifi", false)
set(value) { config.set("dontShowAppManagerDialog", value); mDontShowAppManagerDialog = value }
private var mSyncEnabled: Boolean? = null var dontShowAppManagerDialog by base.config<Boolean>(false)
var enabled: Boolean var lastAppSync by base.config<Long>(0L)
get() { mSyncEnabled = mSyncEnabled ?: config.values.get("syncEnabled", true); return mSyncEnabled ?: true } var notifyAboutUpdates by base.config<Boolean>(true)
set(value) { config.set("syncEnabled", value); mSyncEnabled = value } var webPushEnabled by base.config<Boolean>(true)
private var mWebPushEnabled: Boolean? = null // Quiet Hours
var webPushEnabled: Boolean var quietHoursEnabled by base.config<Boolean>(false)
get() { mWebPushEnabled = mWebPushEnabled ?: config.values.get("webPushEnabled", true); return mWebPushEnabled ?: true } var quietHoursStart by base.config<Time?>(null)
set(value) { config.set("webPushEnabled", value); mWebPushEnabled = value } var quietHoursEnd by base.config<Time?>(null)
var quietDuringLessons by base.config<Boolean>(false)
private var mSyncOnlyWifi: Boolean? = null // FCM Tokens
var onlyWifi: Boolean var tokenApp by base.config<String?>(null)
get() { mSyncOnlyWifi = mSyncOnlyWifi ?: config.values.get("syncOnlyWifi", false); return mSyncOnlyWifi ?: notifyAboutUpdates } var tokenMobidziennik by base.config<String?>(null)
set(value) { config.set("syncOnlyWifi", value); mSyncOnlyWifi = value } var tokenLibrus by base.config<String?>(null)
var tokenVulcan by base.config<String?>(null)
var tokenVulcanHebe by base.config<String?>(null)
private var mSyncInterval: Int? = null var tokenMobidziennikList by base.config<List<Int>> { listOf() }
var interval: Int var tokenLibrusList by base.config<List<Int>> { listOf() }
get() { mSyncInterval = mSyncInterval ?: config.values.get("syncInterval", 60*60); return mSyncInterval ?: 60*60 } var tokenVulcanList by base.config<List<Int>> { listOf() }
set(value) { config.set("syncInterval", value); mSyncInterval = value } var tokenVulcanHebeList by base.config<List<Int>> { listOf() }
private var mNotifyAboutUpdates: Boolean? = null // Register Availability
var notifyAboutUpdates: Boolean private var registerAvailabilityMap by base.config<Map<String, RegisterAvailabilityStatus>>("registerAvailability") { mapOf() }
get() { mNotifyAboutUpdates = mNotifyAboutUpdates ?: config.values.get("notifyAboutUpdates", true); return mNotifyAboutUpdates ?: true } private var registerAvailabilityFlavor by base.config<String?>(null)
set(value) { config.set("notifyAboutUpdates", value); mNotifyAboutUpdates = value }
private var mLastAppSync: Long? = null
var lastAppSync: Long
get() { mLastAppSync = mLastAppSync ?: config.values.get("lastAppSync", 0L); return mLastAppSync ?: 0L }
set(value) { config.set("lastAppSync", value); mLastAppSync = value }
/* ____ _ _ _
/ __ \ (_) | | | |
| | | |_ _ _ ___| |_ | |__ ___ _ _ _ __ ___
| | | | | | | |/ _ \ __| | '_ \ / _ \| | | | '__/ __|
| |__| | |_| | | __/ |_ | | | | (_) | |_| | | \__ \
\___\_\\__,_|_|\___|\__| |_| |_|\___/ \__,_|_| |__*/
private var mQuietHoursEnabled: Boolean? = null
var quietHoursEnabled: Boolean
get() { mQuietHoursEnabled = mQuietHoursEnabled ?: config.values.get("quietHoursEnabled", false); return mQuietHoursEnabled ?: false }
set(value) { config.set("quietHoursEnabled", value); mQuietHoursEnabled = value }
private var mQuietHoursStart: Time? = null
var quietHoursStart: Time?
get() { mQuietHoursStart = mQuietHoursStart ?: config.values.get("quietHoursStart", null as Time?); return mQuietHoursStart }
set(value) { config.set("quietHoursStart", value); mQuietHoursStart = value }
private var mQuietHoursEnd: Time? = null
var quietHoursEnd: Time?
get() { mQuietHoursEnd = mQuietHoursEnd ?: config.values.get("quietHoursEnd", null as Time?); return mQuietHoursEnd }
set(value) { config.set("quietHoursEnd", value); mQuietHoursEnd = value }
private var mQuietDuringLessons: Boolean? = null
var quietDuringLessons: Boolean
get() { mQuietDuringLessons = mQuietDuringLessons ?: config.values.get("quietDuringLessons", false); return mQuietDuringLessons ?: false }
set(value) { config.set("quietDuringLessons", value); mQuietDuringLessons = value }
/* ______ _____ __ __ _______ _
| ____/ ____| \/ | |__ __| | |
| |__ | | | \ / | | | ___ | | _____ _ __ ___
| __|| | | |\/| | | |/ _ \| |/ / _ \ '_ \/ __|
| | | |____| | | | | | (_) | < __/ | | \__ \
|_| \_____|_| |_| |_|\___/|_|\_\___|_| |_|__*/
private var mTokenApp: String? = null
var tokenApp: String?
get() { mTokenApp = mTokenApp ?: config.values.get("tokenApp", null as String?); return mTokenApp }
set(value) { config.set("tokenApp", value); mTokenApp = value }
private var mTokenMobidziennik: String? = null
var tokenMobidziennik: String?
get() { mTokenMobidziennik = mTokenMobidziennik ?: config.values.get("tokenMobidziennik", null as String?); return mTokenMobidziennik }
set(value) { config.set("tokenMobidziennik", value); mTokenMobidziennik = value }
private var mTokenLibrus: String? = null
var tokenLibrus: String?
get() { mTokenLibrus = mTokenLibrus ?: config.values.get("tokenLibrus", null as String?); return mTokenLibrus }
set(value) { config.set("tokenLibrus", value); mTokenLibrus = value }
private var mTokenVulcan: String? = null
var tokenVulcan: String?
get() { mTokenVulcan = mTokenVulcan ?: config.values.get("tokenVulcan", null as String?); return mTokenVulcan }
set(value) { config.set("tokenVulcan", value); mTokenVulcan = value }
private var mTokenVulcanHebe: String? = null
var tokenVulcanHebe: String?
get() { mTokenVulcanHebe = mTokenVulcanHebe ?: config.values.get("tokenVulcanHebe", null as String?); return mTokenVulcanHebe }
set(value) { config.set("tokenVulcanHebe", value); mTokenVulcanHebe = value }
private var mTokenMobidziennikList: List<Int>? = null
var tokenMobidziennikList: List<Int>
get() { mTokenMobidziennikList = mTokenMobidziennikList ?: config.values.getIntList("tokenMobidziennikList", listOf()); return mTokenMobidziennikList ?: listOf() }
set(value) { config.set("tokenMobidziennikList", value); mTokenMobidziennikList = value }
private var mTokenLibrusList: List<Int>? = null
var tokenLibrusList: List<Int>
get() { mTokenLibrusList = mTokenLibrusList ?: config.values.getIntList("tokenLibrusList", listOf()); return mTokenLibrusList ?: listOf() }
set(value) { config.set("tokenLibrusList", value); mTokenLibrusList = value }
private var mTokenVulcanList: List<Int>? = null
var tokenVulcanList: List<Int>
get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() }
set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value }
private var mTokenVulcanHebeList: List<Int>? = null
var tokenVulcanHebeList: List<Int>
get() { mTokenVulcanHebeList = mTokenVulcanHebeList ?: config.values.getIntList("tokenVulcanHebeList", listOf()); return mTokenVulcanHebeList ?: listOf() }
set(value) { config.set("tokenVulcanHebeList", value); mTokenVulcanHebeList = value }
private var mRegisterAvailability: Map<String, RegisterAvailabilityStatus>? = null
var registerAvailability: Map<String, RegisterAvailabilityStatus> var registerAvailability: Map<String, RegisterAvailabilityStatus>
get() { get() {
val flavor = config.values.get("registerAvailabilityFlavor", null as String?) if (BuildConfig.FLAVOR != registerAvailabilityFlavor)
if (BuildConfig.FLAVOR != flavor)
return mapOf() return mapOf()
return registerAvailabilityMap
mRegisterAvailability = mRegisterAvailability ?: config.values.get("registerAvailability", null as String?)?.let { it ->
gson.fromJson(it, object: TypeToken<Map<String, RegisterAvailabilityStatus>>(){}.type)
}
return mRegisterAvailability ?: mapOf()
} }
set(value) { set(value) {
config.setMap("registerAvailability", value) registerAvailabilityMap = value
config.set("registerAvailabilityFlavor", BuildConfig.FLAVOR) registerAvailabilityFlavor = BuildConfig.FLAVOR
mRegisterAvailability = value
} }
} }

View File

@ -4,23 +4,12 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
class ConfigTimetable(private val config: Config) { @Suppress("RemoveExplicitTypeArguments")
private var mBellSyncMultiplier: Int? = null class ConfigTimetable(base: Config) {
var bellSyncMultiplier: Int
get() { mBellSyncMultiplier = mBellSyncMultiplier ?: config.values.get("bellSyncMultiplier", 0); return mBellSyncMultiplier ?: 0 }
set(value) { config.set("bellSyncMultiplier", value); mBellSyncMultiplier = value }
private var mBellSyncDiff: Time? = null var bellSyncMultiplier by base.config<Int>(0)
var bellSyncDiff: Time? var bellSyncDiff by base.config<Time?>(null)
get() { mBellSyncDiff = mBellSyncDiff ?: config.values.get("bellSyncDiff", null as Time?); return mBellSyncDiff } var countInSeconds by base.config<Boolean>(false)
set(value) { config.set("bellSyncDiff", value); mBellSyncDiff = value } }
private var mCountInSeconds: Boolean? = null
var countInSeconds: Boolean
get() { mCountInSeconds = mCountInSeconds ?: config.values.get("countInSeconds", false); return mCountInSeconds ?: false }
set(value) { config.set("countInSeconds", value); mCountInSeconds = value }
}

View File

@ -4,60 +4,32 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.getIntList
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.ext.asNavTargetOrNull
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
class ConfigUI(private val config: Config) { @Suppress("RemoveExplicitTypeArguments")
private var mTheme: Int? = null class ConfigUI(base: Config) {
var theme: Int
get() { mTheme = mTheme ?: config.values.get("theme", 1); return mTheme ?: 1 }
set(value) { config.set("theme", value); mTheme = value }
private var mLanguage: String? = null var theme by base.config<Int>(1)
var language: String? var language by base.config<String?>(null)
get() { mLanguage = mLanguage ?: config.values.get("language", null as String?); return mLanguage }
set(value) { config.set("language", value); mLanguage = value }
private var mHeaderBackground: String? = null var appBackground by base.config<String?>("appBg", null)
var headerBackground: String? var headerBackground by base.config<String?>("headerBg", null)
get() { mHeaderBackground = mHeaderBackground ?: config.values.get("headerBg", null as String?); return mHeaderBackground }
set(value) { config.set("headerBg", value); mHeaderBackground = value }
private var mAppBackground: String? = null var miniMenuVisible by base.config<Boolean>(false)
var appBackground: String? var miniMenuButtons by base.config<Set<NavTarget>> {
get() { mAppBackground = mAppBackground ?: config.values.get("appBg", null as String?); return mAppBackground } setOf(
set(value) { config.set("appBg", value); mAppBackground = value } NavTarget.HOME,
NavTarget.TIMETABLE,
NavTarget.AGENDA,
NavTarget.GRADES,
NavTarget.MESSAGES,
NavTarget.HOMEWORK,
NavTarget.SETTINGS
)
}
var openDrawerOnBackPressed by base.config<Boolean>(false)
private var mMiniMenuVisible: Boolean? = null var bottomSheetOpened by base.config<Boolean>(false)
var miniMenuVisible: Boolean var snowfall by base.config<Boolean>(false)
get() { mMiniMenuVisible = mMiniMenuVisible ?: config.values.get("miniMenuVisible", false); return mMiniMenuVisible ?: false } var eggfall by base.config<Boolean>(false)
set(value) { config.set("miniMenuVisible", value); mMiniMenuVisible = value }
private var mMiniMenuButtons: Set<NavTarget>? = null
var miniMenuButtons: Set<NavTarget>
get() { mMiniMenuButtons = mMiniMenuButtons ?: config.values.getIntList("miniMenuButtons", listOf())?.mapNotNull { it.asNavTargetOrNull() }?.toSet(); return mMiniMenuButtons ?: setOf() }
set(value) { config.set("miniMenuButtons", value.map { it.id }); mMiniMenuButtons = value }
private var mOpenDrawerOnBackPressed: Boolean? = null
var openDrawerOnBackPressed: Boolean
get() { mOpenDrawerOnBackPressed = mOpenDrawerOnBackPressed ?: config.values.get("openDrawerOnBackPressed", false); return mOpenDrawerOnBackPressed ?: false }
set(value) { config.set("openDrawerOnBackPressed", value); mOpenDrawerOnBackPressed = value }
private var mSnowfall: Boolean? = null
var snowfall: Boolean
get() { mSnowfall = mSnowfall ?: config.values.get("snowfall", false); return mSnowfall ?: false }
set(value) { config.set("snowfall", value); mSnowfall = value }
private var mEggfall: Boolean? = null
var eggfall: Boolean
get() { mEggfall = mEggfall ?: config.values.get("eggfall", false); return mEggfall ?: false }
set(value) { config.set("eggfall", value); mEggfall = value }
private var mBottomSheetOpened: Boolean? = null
var bottomSheetOpened: Boolean
get() { mBottomSheetOpened = mBottomSheetOpened ?: config.values.get("bottomSheetOpened", false); return mBottomSheetOpened ?: false }
set(value) { config.set("bottomSheetOpened", value); mBottomSheetOpened = value }
} }

View File

@ -0,0 +1,175 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-21.
*/
package pl.szczodrzynski.edziennik.config
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.reflect.TypeToken
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import java.lang.reflect.ParameterizedType
import java.lang.reflect.WildcardType
import kotlin.reflect.KProperty
private val gson = Gson()
inline fun <reified T> BaseConfig.config(name: String? = null, noinline default: () -> T) = ConfigDelegate(
config = this,
type = T::class.java,
nullable = null is T,
typeToken = object : TypeToken<T>() {},
defaultFunc = default,
defaultValue = null,
fieldName = name,
)
inline fun <reified T> BaseConfig.config(default: T) = ConfigDelegate(
config = this,
type = T::class.java,
nullable = null is T,
typeToken = object : TypeToken<T>() {},
defaultFunc = null,
defaultValue = default,
fieldName = null,
)
inline fun <reified T> BaseConfig.config(name: String? = null, default: T) = ConfigDelegate(
config = this,
type = T::class.java,
nullable = null is T,
typeToken = object : TypeToken<T>() {},
defaultFunc = null,
defaultValue = default,
fieldName = name,
)
@Suppress("UNCHECKED_CAST")
class ConfigDelegate<T>(
private val config: BaseConfig,
private val type: Class<T>,
private val nullable: Boolean,
private val typeToken: TypeToken<T>,
private val defaultFunc: (() -> T)?,
private val defaultValue: T?,
private val fieldName: String?,
) {
private var value: T? = null
private var isInitialized = false
private fun getDefault(): T = when {
defaultFunc != null -> defaultFunc.invoke()
else -> defaultValue as T
}
private fun getGenericType(index: Int = 0): Class<*> {
val parameterizedType = typeToken.type as ParameterizedType
val typeArgument = parameterizedType.actualTypeArguments[index] as WildcardType
return typeArgument.upperBounds[0] as Class<*>
}
operator fun setValue(_thisRef: Any, property: KProperty<*>, newValue: T) {
value = newValue
isInitialized = true
config.set(fieldName ?: property.name, serialize(newValue)?.toString())
}
operator fun getValue(_thisRef: Any, property: KProperty<*>): T {
if (isInitialized)
return value as T
val key = fieldName ?: property.name
if (key !in config.values) {
value = getDefault()
isInitialized = true
return value as T
}
val str = config.values[key]
value = if (str == null && nullable)
null as T
else if (str == null)
getDefault()
else
deserialize(str)
isInitialized = true
return value as T
}
private fun <I> serialize(value: I?, serializeObjects: Boolean = true): Any? {
if (value == null)
return null
return when (value) {
is String -> value
is Date -> value.stringY_m_d
is Time -> value.stringValue
is JsonObject -> value
is JsonArray -> value
// primitives
is Number -> value
is Boolean -> value
// enums, maps & collections
is Enum<*> -> value.toInt()
is Collection<*> -> value.map {
if (it is Number || it is Boolean) it else serialize(it, serializeObjects = false)
}.toJsonElement()
is Map<*, *> -> gson.toJson(value.mapValues { (_, it) ->
if (it is Number || it is Boolean) it else serialize(it, serializeObjects = false)
})
// objects or else
else -> if (serializeObjects) gson.toJson(value) else value
}
}
private fun <I> deserialize(value: String?, type: Class<*> = this.type): I? {
if (value == null)
return null
@Suppress("TYPE_MISMATCH_WARNING")
return when (type) {
String::class.java -> value
Date::class.java -> Date.fromY_m_d(value)
Time::class.java -> Time.fromHms(value)
JsonObject::class.java -> value.toJsonObject()
JsonArray::class.java -> value.toJsonArray()
// primitives
java.lang.Integer::class.java -> value.toIntOrNull()
java.lang.Boolean::class.java -> value.toBooleanStrictOrNull()
java.lang.Long::class.java -> value.toLongOrNull()
java.lang.Float::class.java -> value.toFloatOrNull()
// enums, maps & collections
else -> when {
Enum::class.java.isAssignableFrom(type) -> value.toIntOrNull()?.toEnum(type) as Enum<*>
Collection::class.java.isAssignableFrom(type) -> {
val array = value.toJsonArray()
val genericType = getGenericType()
val list = array?.map {
val str = if (it.isJsonPrimitive) it.asString else it.toString()
deserialize<Any>(str, genericType)
}
when {
List::class.java.isAssignableFrom(type) -> list
Set::class.java.isAssignableFrom(type) -> list?.toSet()
else -> list?.toTypedArray()
}
}
Map::class.java.isAssignableFrom(type) -> {
val obj = value.toJsonObject()
val genericType = getGenericType(index = 1)
val map = obj?.entrySet()?.associate { (key, it) ->
val str = if (it.isJsonPrimitive) it.asString else it.toString()
key to deserialize<Any>(str, genericType)
}
map
}
// objects or else
else -> gson.fromJson(value, type)
}
} as? I
}
}

View File

@ -4,29 +4,20 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.config.db.ConfigEntry import pl.szczodrzynski.edziennik.config.db.ConfigEntry
import pl.szczodrzynski.edziennik.config.utils.ProfileConfigMigration import pl.szczodrzynski.edziennik.config.utils.ProfileConfigMigration
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.config.utils.toHashMap
import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.AppDb
import kotlin.coroutines.CoroutineContext
class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List<ConfigEntry>) : CoroutineScope, AbstractConfig { @Suppress("RemoveExplicitTypeArguments")
class ProfileConfig(
db: AppDb,
profileId: Int,
entries: List<ConfigEntry>?,
) : BaseConfig(db, profileId, entries) {
companion object { companion object {
const val DATA_VERSION = 3 const val DATA_VERSION = 5
} }
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Default
val values: HashMap<String, String?> = hashMapOf()
val grades by lazy { ProfileConfigGrades(this) } val grades by lazy { ProfileConfigGrades(this) }
val ui by lazy { ProfileConfigUI(this) } val ui by lazy { ProfileConfigUI(this) }
val sync by lazy { ProfileConfigSync(this) } val sync by lazy { ProfileConfigSync(this) }
@ -35,26 +26,13 @@ class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List<ConfigEn
val timetable by lazy { ConfigTimetable(this) } val timetable by lazy { ConfigTimetable(this) }
val grades by lazy { ConfigGrades(this) }*/ val grades by lazy { ConfigGrades(this) }*/
private var mDataVersion: Int? = null var dataVersion by config<Int>(DATA_VERSION)
var dataVersion: Int var hash by config<String>("")
get() { mDataVersion = mDataVersion ?: values.get("dataVersion", 0); return mDataVersion ?: 0 }
set(value) { set("dataVersion", value); mDataVersion = value }
private var mHash: String? = null var shareByDefault by config<Boolean>(false)
var hash: String
get() { mHash = mHash ?: values.get("hash", ""); return mHash ?: "" }
set(value) { set("hash", value); mHash = value }
init { init {
rawEntries.toHashMap(profileId, values)
if (dataVersion < DATA_VERSION) if (dataVersion < DATA_VERSION)
ProfileConfigMigration(this) ProfileConfigMigration(this)
} }
override fun set(key: String, value: String?) {
values[key] = value
launch {
db.configDao().add(ConfigEntry(profileId, key, value))
}
}
} }

View File

@ -4,27 +4,11 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get @Suppress("RemoveExplicitTypeArguments")
import pl.szczodrzynski.edziennik.config.utils.set class ProfileConfigAttendance(base: ProfileConfig) {
class ProfileConfigAttendance(private val config: ProfileConfig) { var attendancePageSelection by base.config<Int>(1)
private var mAttendancePageSelection: Int? = null var groupConsecutiveDays by base.config<Boolean>(true)
var attendancePageSelection: Int var showPresenceInMonth by base.config<Boolean>(false)
get() { mAttendancePageSelection = mAttendancePageSelection ?: config.values.get("attendancePageSelection", 1); return mAttendancePageSelection ?: 1 } var useSymbols by base.config<Boolean>(false)
set(value) { config.set("attendancePageSelection", value); mAttendancePageSelection = value }
private var mUseSymbols: Boolean? = null
var useSymbols: Boolean
get() { mUseSymbols = mUseSymbols ?: config.values.get("useSymbols", false); return mUseSymbols ?: false }
set(value) { config.set("useSymbols", value); mUseSymbols = value }
private var mGroupConsecutiveDays: Boolean? = null
var groupConsecutiveDays: Boolean
get() { mGroupConsecutiveDays = mGroupConsecutiveDays ?: config.values.get("groupConsecutiveDays", true); return mGroupConsecutiveDays ?: true }
set(value) { config.set("groupConsecutiveDays", value); mGroupConsecutiveDays = value }
private var mShowPresenceInMonth: Boolean? = null
var showPresenceInMonth: Boolean
get() { mShowPresenceInMonth = mShowPresenceInMonth ?: config.values.get("showPresenceInMonth", false); return mShowPresenceInMonth ?: false }
set(value) { config.set("showPresenceInMonth", value); mShowPresenceInMonth = value }
} }

View File

@ -4,54 +4,19 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.getFloat
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES
class ProfileConfigGrades(private val config: ProfileConfig) { @Suppress("RemoveExplicitTypeArguments")
private var mColorMode: Int? = null class ProfileConfigGrades(base: ProfileConfig) {
var colorMode: Int
get() { mColorMode = mColorMode ?: config.values.get("gradesColorMode", COLOR_MODE_WEIGHTED); return mColorMode ?: COLOR_MODE_WEIGHTED }
set(value) { config.set("gradesColorMode", value); mColorMode = value }
private var mYearAverageMode: Int? = null var averageWithoutWeight by base.config<Boolean>(true)
var yearAverageMode: Int var colorMode by base.config<Int>(COLOR_MODE_WEIGHTED)
get() { mYearAverageMode = mYearAverageMode ?: config.values.get("yearAverageMode", YEAR_ALL_GRADES); return mYearAverageMode ?: YEAR_ALL_GRADES } var dontCountEnabled by base.config<Boolean>(false)
set(value) { config.set("yearAverageMode", value); mYearAverageMode = value } var dontCountGrades by base.config<List<String>> { listOf() }
var hideImproved by base.config<Boolean>(false)
private var mHideImproved: Boolean? = null var hideSticksFromOld by base.config<Boolean>(false)
var hideImproved: Boolean var minusValue by base.config<Float?>(null)
get() { mHideImproved = mHideImproved ?: config.values.get("hideImproved", false); return mHideImproved ?: false } var plusValue by base.config<Float?>(null)
set(value) { config.set("hideImproved", value); mHideImproved = value } var yearAverageMode by base.config<Int>(YEAR_ALL_GRADES)
private var mAverageWithoutWeight: Boolean? = null
var averageWithoutWeight: Boolean
get() { mAverageWithoutWeight = mAverageWithoutWeight ?: config.values.get("averageWithoutWeight", true); return mAverageWithoutWeight ?: true }
set(value) { config.set("averageWithoutWeight", value); mAverageWithoutWeight = value }
private var mPlusValue: Float? = null
var plusValue: Float?
get() { mPlusValue = mPlusValue ?: config.values.getFloat("plusValue"); return mPlusValue }
set(value) { config.set("plusValue", value); mPlusValue = value }
private var mMinusValue: Float? = null
var minusValue: Float?
get() { mMinusValue = mMinusValue ?: config.values.getFloat("minusValue"); return mMinusValue }
set(value) { config.set("minusValue", value); mMinusValue = value }
private var mDontCountEnabled: Boolean? = null
var dontCountEnabled: Boolean
get() { mDontCountEnabled = mDontCountEnabled ?: config.values.get("dontCountEnabled", false); return mDontCountEnabled ?: false }
set(value) { config.set("dontCountEnabled", value); mDontCountEnabled = value }
private var mDontCountGrades: List<String>? = null
var dontCountGrades: List<String>
get() { mDontCountGrades = mDontCountGrades ?: config.values.get("dontCountGrades", listOf()); return mDontCountGrades ?: listOf() }
set(value) { config.set("dontCountGrades", value); mDontCountGrades = value }
private var mHideSticksFromOld: Boolean? = null
var hideSticksFromOld: Boolean
get() { mHideSticksFromOld = mHideSticksFromOld ?: config.values.get("hideSticksFromOld", false); return mHideSticksFromOld ?: false }
set(value) { config.set("hideSticksFromOld", value); mHideSticksFromOld = value }
} }

View File

@ -4,14 +4,14 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.getIntList
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.data.db.enums.NotificationType import pl.szczodrzynski.edziennik.data.db.enums.NotificationType
import pl.szczodrzynski.edziennik.ext.asNotificationTypeOrNull
class ProfileConfigSync(private val config: ProfileConfig) { @Suppress("RemoveExplicitTypeArguments")
private var mNotificationFilter: Set<NotificationType>? = null class ProfileConfigSync(base: ProfileConfig) {
var notificationFilter: Set<NotificationType>
get() { mNotificationFilter = mNotificationFilter ?: config.values.getIntList("notificationFilter", listOf())?.mapNotNull { it.asNotificationTypeOrNull() }?.toSet(); return mNotificationFilter ?: setOf() } var notificationFilter by base.config<Set<NotificationType>> {
set(value) { config.set("notificationFilter", value.map { it.id }); mNotificationFilter = value } NotificationType.values()
.filter { it.enabledByDefault == false }
.toSet()
}
} }

View File

@ -4,89 +4,30 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT
import pl.szczodrzynski.edziennik.ui.home.HomeCardModel import pl.szczodrzynski.edziennik.ui.home.HomeCardModel
class ProfileConfigUI(private val config: ProfileConfig) { @Suppress("RemoveExplicitTypeArguments")
private var mAgendaViewType: Int? = null class ProfileConfigUI(base: ProfileConfig) {
var agendaViewType: Int
get() { mAgendaViewType = mAgendaViewType ?: config.values.get("agendaViewType", 0); return mAgendaViewType ?: AGENDA_DEFAULT }
set(value) { config.set("agendaViewType", value); mAgendaViewType = value }
private var mAgendaCompactMode: Boolean? = null var agendaViewType by base.config<Int>(AGENDA_DEFAULT)
var agendaCompactMode: Boolean var agendaCompactMode by base.config<Boolean>(false)
get() { mAgendaCompactMode = mAgendaCompactMode ?: config.values.get("agendaCompactMode", false); return mAgendaCompactMode ?: false } var agendaGroupByType by base.config<Boolean>(false)
set(value) { config.set("agendaCompactMode", value); mAgendaCompactMode = value } var agendaLessonChanges by base.config<Boolean>(true)
var agendaTeacherAbsence by base.config<Boolean>(true)
var agendaSubjectImportant by base.config<Boolean>(false)
var agendaElearningMark by base.config<Boolean>(false)
var agendaElearningGroup by base.config<Boolean>(true)
private var mAgendaGroupByType: Boolean? = null var homeCards by base.config<List<HomeCardModel>> { listOf() }
var agendaGroupByType: Boolean
get() { mAgendaGroupByType = mAgendaGroupByType ?: config.values.get("agendaGroupByType", false); return mAgendaGroupByType ?: false }
set(value) { config.set("agendaGroupByType", value); mAgendaGroupByType = value }
private var mAgendaLessonChanges: Boolean? = null var messagesGreetingOnCompose by base.config<Boolean>(true)
var agendaLessonChanges: Boolean var messagesGreetingOnReply by base.config<Boolean>(true)
get() { mAgendaLessonChanges = mAgendaLessonChanges ?: config.values.get("agendaLessonChanges", true); return mAgendaLessonChanges ?: true } var messagesGreetingOnForward by base.config<Boolean>(false)
set(value) { config.set("agendaLessonChanges", value); mAgendaLessonChanges = value } var messagesGreetingText by base.config<String?>(null)
private var mAgendaTeacherAbsence: Boolean? = null var timetableShowAttendance by base.config<Boolean>(true)
var agendaTeacherAbsence: Boolean var timetableShowEvents by base.config<Boolean>(true)
get() { mAgendaTeacherAbsence = mAgendaTeacherAbsence ?: config.values.get("agendaTeacherAbsence", true); return mAgendaTeacherAbsence ?: true } var timetableTrimHourRange by base.config<Boolean>(false)
set(value) { config.set("agendaTeacherAbsence", value); mAgendaTeacherAbsence = value } var timetableColorSubjectName by base.config<Boolean>(false)
private var mAgendaElearningMark: Boolean? = null
var agendaElearningMark: Boolean
get() { mAgendaElearningMark = mAgendaElearningMark ?: config.values.get("agendaElearningMark", false); return mAgendaElearningMark ?: false }
set(value) { config.set("agendaElearningMark", value); mAgendaElearningMark = value }
private var mAgendaElearningGroup: Boolean? = null
var agendaElearningGroup: Boolean
get() { mAgendaElearningGroup = mAgendaElearningGroup ?: config.values.get("agendaElearningGroup", true); return mAgendaElearningGroup ?: true }
set(value) { config.set("agendaElearningGroup", value); mAgendaElearningGroup = value }
private var mHomeCards: List<HomeCardModel>? = null
var homeCards: List<HomeCardModel>
get() { mHomeCards = mHomeCards ?: config.values.get("homeCards", listOf(), HomeCardModel::class.java); return mHomeCards ?: listOf() }
set(value) { config.set("homeCards", value); mHomeCards = value }
private var mMessagesGreetingOnCompose: Boolean? = null
var messagesGreetingOnCompose: Boolean
get() { mMessagesGreetingOnCompose = mMessagesGreetingOnCompose ?: config.values.get("messagesGreetingOnCompose", true); return mMessagesGreetingOnCompose ?: true }
set(value) { config.set("messagesGreetingOnCompose", value); mMessagesGreetingOnCompose = value }
private var mMessagesGreetingOnReply: Boolean? = null
var messagesGreetingOnReply: Boolean
get() { mMessagesGreetingOnReply = mMessagesGreetingOnReply ?: config.values.get("messagesGreetingOnReply", true); return mMessagesGreetingOnReply ?: true }
set(value) { config.set("messagesGreetingOnReply", value); mMessagesGreetingOnReply = value }
private var mMessagesGreetingOnForward: Boolean? = null
var messagesGreetingOnForward: Boolean
get() { mMessagesGreetingOnForward = mMessagesGreetingOnForward ?: config.values.get("messagesGreetingOnForward", false); return mMessagesGreetingOnForward ?: false }
set(value) { config.set("messagesGreetingOnForward", value); mMessagesGreetingOnForward = value }
private var mMessagesGreetingText: String? = null
var messagesGreetingText: String?
get() { mMessagesGreetingText = mMessagesGreetingText ?: config.values["messagesGreetingText"]; return mMessagesGreetingText }
set(value) { config.set("messagesGreetingText", value); mMessagesGreetingText = value }
private var mTimetableShowAttendance: Boolean? = null
var timetableShowAttendance: Boolean
get() { mTimetableShowAttendance = mTimetableShowAttendance ?: config.values.get("timetableShowAttendance", true); return mTimetableShowAttendance ?: true }
set(value) { config.set("timetableShowAttendance", value); mTimetableShowAttendance = value }
private var mTimetableShowEvents: Boolean? = null
var timetableShowEvents: Boolean
get() { mTimetableShowEvents = mTimetableShowEvents ?: config.values.get("timetableShowEvents", true); return mTimetableShowEvents ?: true }
set(value) { config.set("timetableShowEvents", value); mTimetableShowEvents = value }
private var mTimetableTrimHourRange: Boolean? = null
var timetableTrimHourRange: Boolean
get() { mTimetableTrimHourRange = mTimetableTrimHourRange ?: config.values.get("timetableTrimHourRange", false); return mTimetableTrimHourRange ?: false }
set(value) { config.set("timetableTrimHourRange", value); mTimetableTrimHourRange = value }
private var mTimetableColorSubjectName: Boolean? = null
var timetableColorSubjectName: Boolean
get() { mTimetableColorSubjectName = mTimetableColorSubjectName ?: config.values.get("timetableColorSubjectName", false); return mTimetableColorSubjectName ?: false }
set(value) { config.set("timetableColorSubjectName", value); mTimetableColorSubjectName = value }
} }

View File

@ -17,7 +17,7 @@ interface ConfigDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun addAll(list: List<ConfigEntry>) fun addAll(list: List<ConfigEntry>)
@Query("SELECT * FROM config WHERE profileId = -1") @Query("SELECT * FROM config")
fun getAllNow(): List<ConfigEntry> fun getAllNow(): List<ConfigEntry>
@Query("SELECT * FROM config WHERE profileId = :profileId") @Query("SELECT * FROM config WHERE profileId = :profileId")
@ -25,4 +25,4 @@ interface ConfigDao {
@Query("DELETE FROM config WHERE profileId = :profileId") @Query("DELETE FROM config WHERE profileId = :profileId")
fun clear(profileId: Int) fun clear(profileId: Int)
} }

View File

@ -16,9 +16,9 @@ import pl.szczodrzynski.edziennik.utils.models.Time
import kotlin.math.abs import kotlin.math.abs
class AppConfigMigrationV3(p: SharedPreferences, config: Config) { class AppConfigMigrationV3(p: SharedPreferences, config: Config) {
init { config.apply { init {
val s = "app.appConfig" val s = "app.appConfig"
if (dataVersion < 1) { config.apply {
ui.theme = p.getString("$s.appTheme", null)?.toIntOrNull() ?: 1 ui.theme = p.getString("$s.appTheme", null)?.toIntOrNull() ?: 1
sync.enabled = p.getString("$s.registerSyncEnabled", null)?.toBoolean() ?: true sync.enabled = p.getString("$s.registerSyncEnabled", null)?.toBoolean() ?: true
sync.interval = p.getString("$s.registerSyncInterval", null)?.toIntOrNull() ?: 3600 sync.interval = p.getString("$s.registerSyncInterval", null)?.toIntOrNull() ?: 3600
@ -37,9 +37,6 @@ class AppConfigMigrationV3(p: SharedPreferences, config: Config) {
NavTarget.HOMEWORK, NavTarget.HOMEWORK,
NavTarget.SETTINGS NavTarget.SETTINGS
) )
dataVersion = 1
}
if (dataVersion < 2) {
devModePassword = p.getString("$s.devModePassword", null).fix() devModePassword = p.getString("$s.devModePassword", null).fix()
sync.tokenApp = p.getString("$s.fcmToken", null).fix() sync.tokenApp = p.getString("$s.fcmToken", null).fix()
timetable.bellSyncMultiplier = p.getString("$s.bellSyncMultiplier", null)?.toIntOrNull() ?: 0 timetable.bellSyncMultiplier = p.getString("$s.bellSyncMultiplier", null)?.toIntOrNull() ?: 0
@ -86,9 +83,8 @@ class AppConfigMigrationV3(p: SharedPreferences, config: Config) {
LoginType.LIBRUS.id -> sync.tokenLibrus = token LoginType.LIBRUS.id -> sync.tokenLibrus = token
} }
} }
dataVersion = 2
} }
}} }
private fun String?.fix(): String? { private fun String?.fix(): String? {
return this?.replace("\"", "")?.let { if (it == "null") null else it } return this?.replace("\"", "")?.let { if (it == "null") null else it }

View File

@ -1,113 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-27.
*/
package pl.szczodrzynski.edziennik.config.utils
import com.google.gson.*
import com.google.gson.reflect.TypeToken
import pl.szczodrzynski.edziennik.config.AbstractConfig
import pl.szczodrzynski.edziennik.config.db.ConfigEntry
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
private val gson = Gson()
fun AbstractConfig.set(key: String, value: Int) {
set(key, value.toString())
}
fun AbstractConfig.set(key: String, value: Boolean) {
set(key, value.toString())
}
fun AbstractConfig.set(key: String, value: Long) {
set(key, value.toString())
}
fun AbstractConfig.set(key: String, value: Float) {
set(key, value.toString())
}
fun AbstractConfig.set(key: String, value: Date?) {
set(key, value?.stringY_m_d)
}
fun AbstractConfig.set(key: String, value: Time?) {
set(key, value?.stringValue)
}
fun AbstractConfig.set(key: String, value: JsonElement?) {
set(key, value?.toString())
}
fun AbstractConfig.set(key: String, value: List<Any>?) {
set(key, value?.let { gson.toJson(it) })
}
fun AbstractConfig.set(key: String, value: Any?) {
set(key, value?.let { gson.toJson(it) })
}
fun AbstractConfig.setStringList(key: String, value: List<String>?) {
set(key, value?.let { gson.toJson(it) })
}
fun AbstractConfig.setIntList(key: String, value: List<Int>?) {
set(key, value?.let { gson.toJson(it) })
}
fun AbstractConfig.setLongList(key: String, value: List<Long>?) {
set(key, value?.let { gson.toJson(it) })
}
fun <K, V> AbstractConfig.setMap(key: String, value: Map<K, V>?) {
set(key, value?.let { gson.toJson(it) })
}
fun HashMap<String, String?>.get(key: String, default: String?): String? {
return this[key] ?: default
}
fun HashMap<String, String?>.get(key: String, default: Boolean): Boolean {
return this[key]?.toBoolean() ?: default
}
fun HashMap<String, String?>.getBooleanOrNull(key: String): Boolean? {
return this[key]?.toBooleanStrictOrNull()
}
fun HashMap<String, String?>.get(key: String, default: Int): Int {
return this[key]?.toIntOrNull() ?: default
}
fun HashMap<String, String?>.get(key: String, default: Long): Long {
return this[key]?.toLongOrNull() ?: default
}
fun HashMap<String, String?>.get(key: String, default: Float): Float {
return this[key]?.toFloatOrNull() ?: default
}
fun HashMap<String, String?>.get(key: String, default: Date?): Date? {
return this[key]?.let { Date.fromY_m_d(it) } ?: default
}
fun HashMap<String, String?>.get(key: String, default: Time?): Time? {
return this[key]?.let { Time.fromHms(it) } ?: default
}
fun HashMap<String, String?>.get(key: String, default: JsonObject?): JsonObject? {
return this[key]?.let { JsonParser().parse(it)?.asJsonObject } ?: default
}
fun HashMap<String, String?>.get(key: String, default: JsonArray?): JsonArray? {
return this[key]?.let { JsonParser().parse(it)?.asJsonArray } ?: default
}
inline fun <reified T> HashMap<String, String?>.get(key: String, default: T?): T? {
return this[key]?.let { Gson().fromJson(it, T::class.java) } ?: default
}
/* !!! cannot use mutable list here - modifying it will not update the DB */
fun <T> HashMap<String, String?>.get(key: String, default: List<T>?, classOfT: Class<T>): List<T>? {
return this[key]?.let { ConfigGsonUtils().deserializeList<T>(gson, it, classOfT) } ?: default
}
fun HashMap<String, String?>.getStringList(key: String, default: List<String>?): List<String>? {
return this[key]?.let { gson.fromJson<List<String>>(it, object: TypeToken<List<String>>(){}.type) } ?: default
}
fun HashMap<String, String?>.getIntList(key: String, default: List<Int>?): List<Int>? {
return this[key]?.let { gson.fromJson<List<Int>>(it, object: TypeToken<List<Int>>(){}.type) } ?: default
}
fun HashMap<String, String?>.getLongList(key: String, default: List<Long>?): List<Long>? {
return this[key]?.let { gson.fromJson<List<Long>>(it, object: TypeToken<List<Long>>(){}.type) } ?: default
}
fun HashMap<String, String?>.getFloat(key: String): Float? {
return this[key]?.toFloatOrNull()
}
fun List<ConfigEntry>.toHashMap(profileId: Int, map: HashMap<String, String?>) {
map.clear()
forEach {
if (it.profileId == profileId)
map[it.key] = it.value
}
}

View File

@ -1,45 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-2.
*/
package pl.szczodrzynski.edziennik.config.utils
import com.google.gson.Gson
import com.google.gson.JsonParser
import pl.szczodrzynski.edziennik.ext.getInt
import pl.szczodrzynski.edziennik.ui.home.HomeCardModel
import pl.szczodrzynski.edziennik.utils.models.Time
class ConfigGsonUtils {
@Suppress("UNCHECKED_CAST")
fun <T> deserializeList(gson: Gson, str: String?, classOfT: Class<T>): List<T> {
val json = JsonParser.parseString(str)
val list: MutableList<T> = mutableListOf()
if (!json.isJsonArray)
return list
json.asJsonArray.forEach { e ->
when (classOfT) {
String::class.java -> {
list += e.asString as T
}
HomeCardModel::class.java -> {
val o = e.asJsonObject
list += HomeCardModel(
o.getInt("profileId", 0),
o.getInt("cardId", 0)
) as T
}
Time::class.java -> {
val o = e.asJsonObject
list += Time(
o.getInt("hour", 0),
o.getInt("minute", 0),
o.getInt("second", 0)
) as T
}
}
}
return list
}
}

View File

@ -5,13 +5,9 @@
package pl.szczodrzynski.edziennik.config.utils package pl.szczodrzynski.edziennik.config.utils
import android.content.Context import android.content.Context
import androidx.core.content.edit
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.config.Config import pl.szczodrzynski.edziennik.config.Config
import pl.szczodrzynski.edziennik.ext.HOUR
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_DATE_DESC
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
import kotlin.math.abs import kotlin.math.abs
@ -23,74 +19,14 @@ class ConfigMigration(app: App, config: Config) {
// migrate appConfig from app version 3.x and lower. // migrate appConfig from app version 3.x and lower.
// Updates dataVersion to level 2. // Updates dataVersion to level 2.
AppConfigMigrationV3(p, config) AppConfigMigrationV3(p, config)
} p.edit {
remove("app.appConfig.appTheme")
if (dataVersion < 2) { }
appVersion = BuildConfig.VERSION_CODE
loginFinished = false
ui.language = null
ui.theme = 1
ui.appBackground = null
ui.headerBackground = null
ui.miniMenuVisible = false
ui.miniMenuButtons = setOf(
NavTarget.HOME,
NavTarget.TIMETABLE,
NavTarget.AGENDA,
NavTarget.GRADES,
NavTarget.MESSAGES,
NavTarget.HOMEWORK,
NavTarget.SETTINGS
)
sync.enabled = true
sync.interval = 1* HOUR.toInt()
sync.notifyAboutUpdates = true
sync.onlyWifi = false
sync.quietHoursEnabled = false
sync.quietHoursStart = null
sync.quietHoursEnd = null
sync.quietDuringLessons = false
sync.tokenApp = null
sync.tokenMobidziennik = null
sync.tokenMobidziennikList = listOf()
sync.tokenLibrus = null
sync.tokenLibrusList = listOf()
sync.tokenVulcan = null
sync.tokenVulcanList = listOf()
timetable.bellSyncMultiplier = 0
timetable.bellSyncDiff = null
timetable.countInSeconds = false
grades.orderBy = ORDER_BY_DATE_DESC
dataVersion = 2
}
if (dataVersion < 3) {
update = null
privacyPolicyAccepted = false
devMode = null
devModePassword = null
appInstalledTime = 0L
appRateSnackbarTime = 0L
dataVersion = 3
}
if (dataVersion < 10) {
ui.openDrawerOnBackPressed = false
ui.snowfall = false
ui.bottomSheetOpened = false
sync.dontShowAppManagerDialog = false
sync.webPushEnabled = true
sync.lastAppSync = 0L
dataVersion = 10
} }
if (dataVersion < 11) { if (dataVersion < 11) {
val startMillis = config.values.get("quietHoursStart", 0L) val startMillis = config.values["quietHoursStart"]?.toLongOrNull() ?: 0L
val endMillis = config.values.get("quietHoursEnd", 0L) val endMillis = config.values["quietHoursEnd"]?.toLongOrNull() ?: 0L
if (startMillis > 0) { if (startMillis > 0) {
try { try {
sync.quietHoursStart = Time.fromMillis(abs(startMillis)) sync.quietHoursStart = Time.fromMillis(abs(startMillis))
@ -107,5 +43,7 @@ class ConfigMigration(app: App, config: Config) {
dataVersion = 11 dataVersion = 11
} }
hash = "invalid"
}} }}
} }

View File

@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.config.utils
import pl.szczodrzynski.edziennik.config.ProfileConfig import pl.szczodrzynski.edziennik.config.ProfileConfig
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT
import pl.szczodrzynski.edziennik.data.db.enums.NotificationType import pl.szczodrzynski.edziennik.data.db.enums.NotificationType
import pl.szczodrzynski.edziennik.data.db.enums.SchoolType
import pl.szczodrzynski.edziennik.ui.home.HomeCard import pl.szczodrzynski.edziennik.ui.home.HomeCard
import pl.szczodrzynski.edziennik.ui.home.HomeCardModel import pl.szczodrzynski.edziennik.ui.home.HomeCardModel
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED
@ -15,20 +16,7 @@ import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_AL
class ProfileConfigMigration(config: ProfileConfig) { class ProfileConfigMigration(config: ProfileConfig) {
init { config.apply { init { config.apply {
if (dataVersion < 1) { val profile = db.profileDao().getByIdNow(profileId ?: -1)
grades.colorMode = COLOR_MODE_WEIGHTED
grades.yearAverageMode = YEAR_ALL_GRADES
grades.hideImproved = false
grades.averageWithoutWeight = true
grades.plusValue = null
grades.minusValue = null
grades.dontCountEnabled = false
grades.dontCountGrades = listOf()
ui.agendaViewType = AGENDA_DEFAULT
// no migration for ui.homeCards
dataVersion = 1
}
if (dataVersion < 2) { if (dataVersion < 2) {
sync.notificationFilter = sync.notificationFilter + NotificationType.TEACHER_ABSENCE sync.notificationFilter = sync.notificationFilter + NotificationType.TEACHER_ABSENCE
@ -38,12 +26,36 @@ class ProfileConfigMigration(config: ProfileConfig) {
if (dataVersion < 3) { if (dataVersion < 3) {
if (ui.homeCards.isNotEmpty()) { if (ui.homeCards.isNotEmpty()) {
ui.homeCards = ui.homeCards.toMutableList().also { ui.homeCards = ui.homeCards + HomeCardModel(
it.add(HomeCardModel(config.profileId, HomeCard.CARD_NOTES)) profileId = config.profileId ?: -1,
} cardId = HomeCard.CARD_NOTES,
)
} }
dataVersion = 3 dataVersion = 3
} }
if (dataVersion < 4) {
// switch to new event types (USOS)
dataVersion = 4
if (profile?.loginStoreType?.schoolType == SchoolType.UNIVERSITY) {
db.eventTypeDao().clear(profileId ?: -1)
db.eventTypeDao().addDefaultTypes(profile)
}
}
if (dataVersion < 5) {
// update USOS event types and the appropriate events (2022-12-25)
dataVersion = 5
if (profile?.loginStoreType?.schoolType == SchoolType.UNIVERSITY) {
db.eventTypeDao().getAllWithDefaults(profile)
// wejściówka (4) -> kartkówka (3)
db.eventDao().getRawNow("UPDATE events SET eventType = 3 WHERE profileId = $profileId AND eventType = 4;")
// zadanie (6) -> zadanie domowe (-1)
db.eventDao().getRawNow("UPDATE events SET eventType = -1 WHERE profileId = $profileId AND eventType = 6;")
}
}
}} }}
} }

View File

@ -26,9 +26,10 @@ val LIBRUS_USER_AGENT = "${SYSTEM_USER_AGENT}LibrusMobileApp"
const val SYNERGIA_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0" const val SYNERGIA_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0"
const val LIBRUS_CLIENT_ID = "VaItV6oRutdo8fnjJwysnTjVlvaswf52ZqmXsJGP" const val LIBRUS_CLIENT_ID = "VaItV6oRutdo8fnjJwysnTjVlvaswf52ZqmXsJGP"
const val LIBRUS_REDIRECT_URL = "app://librus" const val LIBRUS_REDIRECT_URL = "app://librus"
const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/oauth2/authorize?client_id=$LIBRUS_CLIENT_ID&redirect_uri=$LIBRUS_REDIRECT_URL&response_type=code" const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/konto-librus/redirect/dru"
const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/rodzina/login/action" const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/konto-librus/login/action"
const val LIBRUS_TOKEN_URL = "https://portal.librus.pl/oauth2/access_token" const val LIBRUS_TOKEN_URL = "https://portal.librus.pl/oauth2/access_token"
const val LIBRUS_HEADER = "pl.librus.synergiaDru2"
const val LIBRUS_ACCOUNT_URL = "/v3/SynergiaAccounts/fresh/" // + login const val LIBRUS_ACCOUNT_URL = "/v3/SynergiaAccounts/fresh/" // + login
const val LIBRUS_ACCOUNTS_URL = "/v3/SynergiaAccounts" const val LIBRUS_ACCOUNTS_URL = "/v3/SynergiaAccounts"
@ -59,9 +60,6 @@ const val LIBRUS_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action="
const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile" const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile"
const val LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL = "https://synergia.librus.pl/wiadomosci/pobierz_zalacznik" const val LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL = "https://synergia.librus.pl/wiadomosci/pobierz_zalacznik"
const val LIBRUS_PORTAL_RECAPTCHA_KEY = "6Lf48moUAAAAAB9ClhdvHr46gRWR"
const val LIBRUS_PORTAL_RECAPTCHA_REFERER = "https://portal.librus.pl/rodzina/login"
val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT

View File

@ -24,6 +24,25 @@ object Regexes {
"""^\[META:([A-z0-9-&=]+)]""".toRegex() """^\[META:([A-z0-9-&=]+)]""".toRegex()
} }
val HTML_INPUT_HIDDEN by lazy {
"""<input .*?type="hidden".+?>""".toRegex()
}
val HTML_INPUT_NAME by lazy {
"""name="(.+?)"""".toRegex()
}
val HTML_INPUT_VALUE by lazy {
"""value="(.+?)"""".toRegex()
}
val HTML_CSRF_TOKEN by lazy {
"""name="csrf-token" content="([A-z0-9=+/\-_]+?)"""".toRegex()
}
val HTML_FORM_ACTION by lazy {
"""<form .*?action="(.+?)"""".toRegex()
}
val HTML_RECAPTCHA_KEY by lazy {
"""data-sitekey="(.+?)"""".toRegex()
}
val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy { val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy {

View File

@ -9,6 +9,7 @@ import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_ARCHIVED import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_ARCHIVED
import pl.szczodrzynski.edziennik.data.api.edziennik.demo.Demo
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.Librus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.Librus
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie
@ -28,6 +29,8 @@ import pl.szczodrzynski.edziennik.data.db.enums.LoginType
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.ext.isBeforeYear
import pl.szczodrzynski.edziennik.ext.shouldArchive
import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
@ -118,6 +121,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
LoginType.PODLASIE -> Podlasie(app, profile, loginStore, taskCallback) LoginType.PODLASIE -> Podlasie(app, profile, loginStore, taskCallback)
LoginType.TEMPLATE -> Template(app, profile, loginStore, taskCallback) LoginType.TEMPLATE -> Template(app, profile, loginStore, taskCallback)
LoginType.USOS -> Usos(app, profile, loginStore, taskCallback) LoginType.USOS -> Usos(app, profile, loginStore, taskCallback)
LoginType.DEMO -> Demo(app, profile, loginStore, taskCallback)
else -> null else -> null
} }
if (edziennikInterface == null) { if (edziennikInterface == null) {

View File

@ -52,28 +52,29 @@ class ProfileArchiver(val app: App, val profile: Profile) {
when (profile.loginStoreType) { when (profile.loginStoreType) {
LoginType.LIBRUS -> { LoginType.LIBRUS -> {
profile.removeStudentData("isPremium") profile.studentData.remove("isPremium")
profile.removeStudentData("pushDeviceId") profile.studentData.remove("pushDeviceId")
profile.removeStudentData("startPointsSemester1") profile.studentData.remove("startPointsSemester1")
profile.removeStudentData("startPointsSemester2") profile.studentData.remove("startPointsSemester2")
profile.removeStudentData("enablePointGrades") profile.studentData.remove("enablePointGrades")
profile.removeStudentData("enableDescriptiveGrades") profile.studentData.remove("enableDescriptiveGrades")
} }
LoginType.MOBIDZIENNIK -> {} LoginType.MOBIDZIENNIK -> {}
LoginType.VULCAN -> { LoginType.VULCAN -> {
// DataVulcan.isApiLoginValid() returns false so it will update the semester // DataVulcan.isApiLoginValid() returns false so it will update the semester
profile.removeStudentData("currentSemesterEndDate") profile.studentData.remove("currentSemesterEndDate")
profile.removeStudentData("studentSemesterId") profile.studentData.remove("studentSemesterId")
profile.removeStudentData("studentSemesterNumber") profile.studentData.remove("studentSemesterNumber")
profile.removeStudentData("semester1Id") profile.studentData.remove("semester1Id")
profile.removeStudentData("semester2Id") profile.studentData.remove("semester2Id")
profile.removeStudentData("studentClassId") profile.studentData.remove("studentClassId")
} }
LoginType.IDZIENNIK -> { LoginType.IDZIENNIK -> {
profile.removeStudentData("schoolYearId") profile.studentData.remove("schoolYearId")
} }
LoginType.EDUDZIENNIK -> {} LoginType.EDUDZIENNIK -> {}
LoginType.PODLASIE -> {} LoginType.PODLASIE -> {}
LoginType.USOS -> {}
LoginType.DEMO -> {} LoginType.DEMO -> {}
LoginType.TEMPLATE -> {} LoginType.TEMPLATE -> {}
} }

View File

@ -0,0 +1,84 @@
/*
* Copyright (c) Kuba Szczodrzyński 2024-7-8.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.demo
import com.google.gson.JsonObject
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
import pl.szczodrzynski.edziennik.data.db.enums.LoginType
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
class Demo(
val app: App,
val profile: Profile?,
val loginStore: LoginStore,
val callback: EdziennikCallback,
) : EdziennikInterface {
private fun completed() {
callback.onCompleted()
}
override fun sync(
featureTypes: Set<FeatureType>?,
onlyEndpoints: Set<Int>?,
arguments: JsonObject?,
) = completed()
override fun getMessage(message: MessageFull) =
completed()
override fun sendMessage(recipients: Set<Teacher>, subject: String, text: String) =
completed()
override fun markAllAnnouncementsAsRead() =
completed()
override fun getAnnouncement(announcement: AnnouncementFull) =
completed()
override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) =
completed()
override fun getRecipientList() =
completed()
override fun getEvent(eventFull: EventFull) =
completed()
override fun firstLogin() {
val profile = Profile(
id = loginStore.id,
loginStoreId = loginStore.id,
loginStoreType = LoginType.DEMO,
name = "Jan Szkolny",
subname = "Szkolny.eu",
studentNameLong = "Jan Szkolny",
studentNameShort = "Jan S.",
accountName = null,
)
profile.apply {
empty = false
syncEnabled = false
registration = Profile.REGISTRATION_DISABLED
studentClassName = "1A"
userCode = "nologin:1234"
dateYearEnd.month = 8
}
EventBus.getDefault().postSticky(FirstLoginFinishedEvent(listOf(profile), loginStore))
completed()
}
override fun cancel() {}
}

View File

@ -10,7 +10,9 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod
import pl.szczodrzynski.edziennik.ext.currentTimeUnix import pl.szczodrzynski.edziennik.ext.currentTimeUnix
import pl.szczodrzynski.edziennik.ext.getStudentData
import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.ext.set
class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
@ -117,7 +119,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mApiLogin: String? = null private var mApiLogin: String? = null
var apiLogin: String? var apiLogin: String?
get() { mApiLogin = mApiLogin ?: profile?.getStudentData("accountLogin", null); return mApiLogin } get() { mApiLogin = mApiLogin ?: profile?.getStudentData("accountLogin", null); return mApiLogin }
set(value) { profile?.putStudentData("accountLogin", value); mApiLogin = value } set(value) { profile["accountLogin"] = value; mApiLogin = value }
/** /**
* A Synergia password. * A Synergia password.
* Used: for login (API Login Method) in Synergia mode. * Used: for login (API Login Method) in Synergia mode.
@ -126,7 +128,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mApiPassword: String? = null private var mApiPassword: String? = null
var apiPassword: String? var apiPassword: String?
get() { mApiPassword = mApiPassword ?: profile?.getStudentData("accountPassword", null); return mApiPassword } get() { mApiPassword = mApiPassword ?: profile?.getStudentData("accountPassword", null); return mApiPassword }
set(value) { profile?.putStudentData("accountPassword", value); mApiPassword = value } set(value) { profile["accountPassword"] = value; mApiPassword = value }
/** /**
* A JST login Code. * A JST login Code.
@ -135,7 +137,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mApiCode: String? = null private var mApiCode: String? = null
var apiCode: String? var apiCode: String?
get() { mApiCode = mApiCode ?: loginStore.getLoginData("accountCode", null); return mApiCode } get() { mApiCode = mApiCode ?: loginStore.getLoginData("accountCode", null); return mApiCode }
set(value) { profile?.putStudentData("accountCode", value); mApiCode = value } set(value) { profile["accountCode"] = value; mApiCode = value }
/** /**
* A JST login PIN. * A JST login PIN.
* Used only during first login in JST mode. * Used only during first login in JST mode.
@ -143,7 +145,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mApiPin: String? = null private var mApiPin: String? = null
var apiPin: String? var apiPin: String?
get() { mApiPin = mApiPin ?: loginStore.getLoginData("accountPin", null); return mApiPin } get() { mApiPin = mApiPin ?: loginStore.getLoginData("accountPin", null); return mApiPin }
set(value) { profile?.putStudentData("accountPin", value); mApiPin = value } set(value) { profile["accountPin"] = value; mApiPin = value }
/** /**
* A Synergia API access token. * A Synergia API access token.
@ -154,7 +156,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mApiAccessToken: String? = null private var mApiAccessToken: String? = null
var apiAccessToken: String? var apiAccessToken: String?
get() { mApiAccessToken = mApiAccessToken ?: profile?.getStudentData("accountToken", null); return mApiAccessToken } get() { mApiAccessToken = mApiAccessToken ?: profile?.getStudentData("accountToken", null); return mApiAccessToken }
set(value) { mApiAccessToken = value; profile?.putStudentData("accountToken", value) ?: return; } set(value) { mApiAccessToken = value; profile["accountToken"] = value ?: return; }
/** /**
* A Synergia API refresh token. * A Synergia API refresh token.
* Used when refreshing the [apiAccessToken] in JST, Synergia modes. * Used when refreshing the [apiAccessToken] in JST, Synergia modes.
@ -162,7 +164,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mApiRefreshToken: String? = null private var mApiRefreshToken: String? = null
var apiRefreshToken: String? var apiRefreshToken: String?
get() { mApiRefreshToken = mApiRefreshToken ?: profile?.getStudentData("accountRefreshToken", null); return mApiRefreshToken } get() { mApiRefreshToken = mApiRefreshToken ?: profile?.getStudentData("accountRefreshToken", null); return mApiRefreshToken }
set(value) { mApiRefreshToken = value; profile?.putStudentData("accountRefreshToken", value) ?: return; } set(value) { mApiRefreshToken = value; profile["accountRefreshToken"] = value ?: return; }
/** /**
* The expiry time for [apiAccessToken], as a UNIX timestamp. * The expiry time for [apiAccessToken], as a UNIX timestamp.
* Used when refreshing the [apiAccessToken] in JST, Synergia modes. * Used when refreshing the [apiAccessToken] in JST, Synergia modes.
@ -171,7 +173,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mApiTokenExpiryTime: Long? = null private var mApiTokenExpiryTime: Long? = null
var apiTokenExpiryTime: Long var apiTokenExpiryTime: Long
get() { mApiTokenExpiryTime = mApiTokenExpiryTime ?: profile?.getStudentData("accountTokenTime", 0L); return mApiTokenExpiryTime ?: 0L } get() { mApiTokenExpiryTime = mApiTokenExpiryTime ?: profile?.getStudentData("accountTokenTime", 0L); return mApiTokenExpiryTime ?: 0L }
set(value) { mApiTokenExpiryTime = value; profile?.putStudentData("accountTokenTime", value) ?: return; } set(value) { mApiTokenExpiryTime = value; profile["accountTokenTime"] = value; }
/** /**
* A push device ID, generated by Librus when registering * A push device ID, generated by Librus when registering
@ -181,7 +183,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mPushDeviceId: Int? = null private var mPushDeviceId: Int? = null
var pushDeviceId: Int var pushDeviceId: Int
get() { mPushDeviceId = mPushDeviceId ?: profile?.getStudentData("pushDeviceId", 0); return mPushDeviceId ?: 0 } get() { mPushDeviceId = mPushDeviceId ?: profile?.getStudentData("pushDeviceId", 0); return mPushDeviceId ?: 0 }
set(value) { mPushDeviceId = value; profile?.putStudentData("pushDeviceId", value) ?: return; } set(value) { mPushDeviceId = value; profile["pushDeviceId"] = value; }
/* _____ _ /* _____ _
/ ____| (_) / ____| (_)
@ -198,7 +200,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mSynergiaSessionId: String? = null private var mSynergiaSessionId: String? = null
var synergiaSessionId: String? var synergiaSessionId: String?
get() { mSynergiaSessionId = mSynergiaSessionId ?: profile?.getStudentData("accountSID", null); return mSynergiaSessionId } get() { mSynergiaSessionId = mSynergiaSessionId ?: profile?.getStudentData("accountSID", null); return mSynergiaSessionId }
set(value) { profile?.putStudentData("accountSID", value) ?: return; mSynergiaSessionId = value } set(value) { profile["accountSID"] = value; mSynergiaSessionId = value }
/** /**
* The expiry time for [synergiaSessionId], as a UNIX timestamp. * The expiry time for [synergiaSessionId], as a UNIX timestamp.
* Used in endpoints with Synergia login method. * Used in endpoints with Synergia login method.
@ -207,7 +209,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mSynergiaSessionIdExpiryTime: Long? = null private var mSynergiaSessionIdExpiryTime: Long? = null
var synergiaSessionIdExpiryTime: Long var synergiaSessionIdExpiryTime: Long
get() { mSynergiaSessionIdExpiryTime = mSynergiaSessionIdExpiryTime ?: profile?.getStudentData("accountSIDTime", 0L); return mSynergiaSessionIdExpiryTime ?: 0L } get() { mSynergiaSessionIdExpiryTime = mSynergiaSessionIdExpiryTime ?: profile?.getStudentData("accountSIDTime", 0L); return mSynergiaSessionIdExpiryTime ?: 0L }
set(value) { profile?.putStudentData("accountSIDTime", value) ?: return; mSynergiaSessionIdExpiryTime = value } set(value) { profile["accountSIDTime"] = value; mSynergiaSessionIdExpiryTime = value }
/** /**
@ -217,7 +219,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mMessagesSessionId: String? = null private var mMessagesSessionId: String? = null
var messagesSessionId: String? var messagesSessionId: String?
get() { mMessagesSessionId = mMessagesSessionId ?: profile?.getStudentData("messagesSID", null); return mMessagesSessionId } get() { mMessagesSessionId = mMessagesSessionId ?: profile?.getStudentData("messagesSID", null); return mMessagesSessionId }
set(value) { profile?.putStudentData("messagesSID", value) ?: return; mMessagesSessionId = value } set(value) { profile["messagesSID"] = value; mMessagesSessionId = value }
/** /**
* The expiry time for [messagesSessionId], as a UNIX timestamp. * The expiry time for [messagesSessionId], as a UNIX timestamp.
* Used in endpoints with Messages login method. * Used in endpoints with Messages login method.
@ -226,7 +228,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mMessagesSessionIdExpiryTime: Long? = null private var mMessagesSessionIdExpiryTime: Long? = null
var messagesSessionIdExpiryTime: Long var messagesSessionIdExpiryTime: Long
get() { mMessagesSessionIdExpiryTime = mMessagesSessionIdExpiryTime ?: profile?.getStudentData("messagesSIDTime", 0L); return mMessagesSessionIdExpiryTime ?: 0L } get() { mMessagesSessionIdExpiryTime = mMessagesSessionIdExpiryTime ?: profile?.getStudentData("messagesSIDTime", 0L); return mMessagesSessionIdExpiryTime ?: 0L }
set(value) { profile?.putStudentData("messagesSIDTime", value) ?: return; mMessagesSessionIdExpiryTime = value } set(value) { profile["messagesSIDTime"] = value; mMessagesSessionIdExpiryTime = value }
/* ____ _ _ /* ____ _ _
/ __ \| | | | / __ \| | | |
@ -236,42 +238,42 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
\____/ \__|_| |_|\___|*/ \____/ \__|_| |_|\___|*/
var isPremium var isPremium
get() = profile?.getStudentData("isPremium", false) ?: false get() = profile?.getStudentData("isPremium", false) ?: false
set(value) { profile?.putStudentData("isPremium", value) } set(value) { profile["isPremium"] = value }
private var mSchoolName: String? = null private var mSchoolName: String? = null
var schoolName: String? var schoolName: String?
get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName } get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName }
set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolName = value } set(value) { profile["schoolName"] = value; mSchoolName = value }
private var mUnitId: Long? = null private var mUnitId: Long? = null
var unitId: Long var unitId: Long
get() { mUnitId = mUnitId ?: profile?.getStudentData("unitId", 0L); return mUnitId ?: 0L } get() { mUnitId = mUnitId ?: profile?.getStudentData("unitId", 0L); return mUnitId ?: 0L }
set(value) { profile?.putStudentData("unitId", value) ?: return; mUnitId = value } set(value) { profile["unitId"] = value; mUnitId = value }
private var mStartPointsSemester1: Int? = null private var mStartPointsSemester1: Int? = null
var startPointsSemester1: Int var startPointsSemester1: Int
get() { mStartPointsSemester1 = mStartPointsSemester1 ?: profile?.getStudentData("startPointsSemester1", 0); return mStartPointsSemester1 ?: 0 } get() { mStartPointsSemester1 = mStartPointsSemester1 ?: profile?.getStudentData("startPointsSemester1", 0); return mStartPointsSemester1 ?: 0 }
set(value) { profile?.putStudentData("startPointsSemester1", value) ?: return; mStartPointsSemester1 = value } set(value) { profile["startPointsSemester1"] = value; mStartPointsSemester1 = value }
private var mStartPointsSemester2: Int? = null private var mStartPointsSemester2: Int? = null
var startPointsSemester2: Int var startPointsSemester2: Int
get() { mStartPointsSemester2 = mStartPointsSemester2 ?: profile?.getStudentData("startPointsSemester2", 0); return mStartPointsSemester2 ?: 0 } get() { mStartPointsSemester2 = mStartPointsSemester2 ?: profile?.getStudentData("startPointsSemester2", 0); return mStartPointsSemester2 ?: 0 }
set(value) { profile?.putStudentData("startPointsSemester2", value) ?: return; mStartPointsSemester2 = value } set(value) { profile["startPointsSemester2"] = value; mStartPointsSemester2 = value }
private var mEnablePointGrades: Boolean? = null private var mEnablePointGrades: Boolean? = null
var enablePointGrades: Boolean var enablePointGrades: Boolean
get() { mEnablePointGrades = mEnablePointGrades ?: profile?.getStudentData("enablePointGrades", true); return mEnablePointGrades ?: true } get() { mEnablePointGrades = mEnablePointGrades ?: profile?.getStudentData("enablePointGrades", true); return mEnablePointGrades ?: true }
set(value) { profile?.putStudentData("enablePointGrades", value) ?: return; mEnablePointGrades = value } set(value) { profile["enablePointGrades"] = value; mEnablePointGrades = value }
private var mEnableDescriptiveGrades: Boolean? = null private var mEnableDescriptiveGrades: Boolean? = null
var enableDescriptiveGrades: Boolean var enableDescriptiveGrades: Boolean
get() { mEnableDescriptiveGrades = mEnableDescriptiveGrades ?: profile?.getStudentData("enableDescriptiveGrades", true); return mEnableDescriptiveGrades ?: true } get() { mEnableDescriptiveGrades = mEnableDescriptiveGrades ?: profile?.getStudentData("enableDescriptiveGrades", true); return mEnableDescriptiveGrades ?: true }
set(value) { profile?.putStudentData("enableDescriptiveGrades", value) ?: return; mEnableDescriptiveGrades = value } set(value) { profile["enableDescriptiveGrades"] = value; mEnableDescriptiveGrades = value }
private var mTimetableNotPublic: Boolean? = null private var mTimetableNotPublic: Boolean? = null
var timetableNotPublic: Boolean var timetableNotPublic: Boolean
get() { mTimetableNotPublic = mTimetableNotPublic ?: profile?.getStudentData("timetableNotPublic", false); return mTimetableNotPublic ?: false } get() { mTimetableNotPublic = mTimetableNotPublic ?: profile?.getStudentData("timetableNotPublic", false); return mTimetableNotPublic ?: false }
set(value) { profile?.putStudentData("timetableNotPublic", value) ?: return; mTimetableNotPublic = value } set(value) { profile["timetableNotPublic"] = value; mTimetableNotPublic = value }
/** /**
* Set to false when Recaptcha helper doesn't provide a working token. * Set to false when Recaptcha helper doesn't provide a working token.

View File

@ -36,7 +36,7 @@ class LibrusApiNotices(override val data: DataLibrus,
val id = note.getLong("Id") ?: return@forEach val id = note.getLong("Id") ?: return@forEach
val text = note.getString("Text") ?: "" val text = note.getString("Text") ?: ""
val categoryId = note.getJsonObject("Category")?.getLong("Id") ?: -1 val categoryId = note.getJsonObject("Category")?.getLong("Id") ?: -1
val teacherId = note.getJsonObject("AddedBy")?.getLong("Id") ?: -1 val teacherId = note.getJsonObject("Teacher")?.getLong("Id") ?: -1
val addedDate = note.getString("Date")?.let { Date.fromY_m_d(it) } ?: return@forEach val addedDate = note.getString("Date")?.let { Date.fromY_m_d(it) } ?: return@forEach
val type = when (note.getInt("Positive")) { val type = when (note.getInt("Positive")) {

View File

@ -190,6 +190,7 @@ class LibrusApiTimetables(override val data: DataLibrus,
} }
lessonObject.id = lessonObject.buildId() lessonObject.id = lessonObject.buildId()
lessonObject.ownerId = lessonObject.buildOwnerId()
val seen = profile.empty || lessonDate < Date.getToday() val seen = profile.empty || lessonDate < Date.getToday()

View File

@ -125,7 +125,7 @@ class LibrusMessagesGetMessage(override val data: DataLibrus,
val receiverId = teacher?.id ?: -1 val receiverId = teacher?.id ?: -1
teacher?.loginId = receiverLoginId teacher?.loginId = receiverLoginId
val readDateText = message.select("readed").text() val readDateText = receiver.select("readed").text()
val readDate = when (readDateText.isNotNullNorEmpty()) { val readDate = when (readDateText.isNotNullNorEmpty()) {
true -> Date.fromIso(readDateText) true -> Date.fromIso(readDateText)
else -> 0 else -> 0

View File

@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
import pl.szczodrzynski.edziennik.ext.HOUR import pl.szczodrzynski.edziennik.ext.HOUR
import pl.szczodrzynski.edziennik.ext.get import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.ext.getSemesterStart
import pl.szczodrzynski.edziennik.ext.singleOrNull import pl.szczodrzynski.edziennik.ext.singleOrNull
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date

View File

@ -24,6 +24,9 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
private const val TAG = "LoginLibrusPortal" private const val TAG = "LoginLibrusPortal"
} }
// loop failsafe
private var loginPerformed = false
init { run { init { run {
if (data.loginStore.mode != LoginMode.LIBRUS_EMAIL) { if (data.loginStore.mode != LoginMode.LIBRUS_EMAIL) {
data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE)) data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE))
@ -33,6 +36,7 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
return@run return@run
} }
loginPerformed = false
// succeed having a non-expired access token and a refresh token // succeed having a non-expired access token and a refresh token
if (data.isPortalLoginValid()) { if (data.isPortalLoginValid()) {
@ -58,18 +62,23 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
} }
}} }}
private fun authorize(url: String?) { private fun authorize(url: String, referer: String? = null) {
d(TAG, "Request: Librus/Login/Portal - $url") d(TAG, "Request: Librus/Login/Portal - $url")
Request.builder() Request.builder()
.url(url) .url(url)
.userAgent(LIBRUS_USER_AGENT) .userAgent(LIBRUS_USER_AGENT)
.also {
if (referer != null)
it.addHeader("Referer", referer)
}
.addHeader("X-Requested-With", LIBRUS_HEADER)
.withClient(data.app.httpLazy) .withClient(data.app.httpLazy)
.callback(object : TextCallbackHandler() { .callback(object : TextCallbackHandler() {
override fun onSuccess(text: String, response: Response) { override fun onSuccess(text: String, response: Response) {
val location = response.headers().get("Location") val location = response.headers().get("Location")
if (location != null) { if (location != null) {
val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([A-z0-9]+?)$", Pattern.DOTALL or Pattern.MULTILINE).matcher(location) val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([^&?]+)", Pattern.DOTALL or Pattern.MULTILINE).matcher(location)
when { when {
authMatcher.find() -> { authMatcher.find() -> {
accessToken(authMatcher.group(1), null) accessToken(authMatcher.group(1), null)
@ -83,16 +92,31 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
authorize(location) authorize(location)
} }
} }
} else { return
val csrfMatcher = Pattern.compile("name=\"csrf-token\" content=\"([A-z0-9=+/\\-_]+?)\"", Pattern.DOTALL).matcher(text) }
if (csrfMatcher.find()) {
login(csrfMatcher.group(1) ?: "") if (checkError(text, response))
} else { return
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_CSRF_MISSING)
.withResponse(response) var loginUrl = if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL
.withApiResponse(text)) val csrfToken = Regexes.HTML_CSRF_TOKEN.find(text)?.get(1) ?: ""
for (match in Regexes.HTML_FORM_ACTION.findAll(text)) {
val form = match.value.lowercase()
if ("login" in form && "post" in form) {
loginUrl = match[1]
} }
} }
val params = mutableMapOf<String, String>()
for (match in Regexes.HTML_INPUT_HIDDEN.findAll(text)) {
val input = match.value
val name = Regexes.HTML_INPUT_NAME.find(input)?.get(1) ?: continue
val value = Regexes.HTML_INPUT_VALUE.find(input)?.get(1) ?: continue
params[name] = value
}
login(url = loginUrl, referer = url, csrfToken, params)
} }
override fun onFailure(response: Response, throwable: Throwable) { override fun onFailure(response: Response, throwable: Throwable) {
@ -105,8 +129,54 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
.enqueue() .enqueue()
} }
private fun login(csrfToken: String) { private fun checkError(text: String, response: Response): Boolean {
d(TAG, "Request: Librus/Login/Portal - ${if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL}") when {
text.contains("librus_account_settings_main") -> return false
text.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED
text.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
text.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
else -> null // no error for now
}?.let { errorCode ->
data.error(ApiError(TAG, errorCode)
.withApiResponse(text)
.withResponse(response))
return true
}
if ("robotem" in text || "g-recaptcha" in text || "captchaValidate" in text) {
val siteKey = Regexes.HTML_RECAPTCHA_KEY.find(text)?.get(1)
if (siteKey == null) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR)
.withApiResponse(text)
.withResponse(response))
return true
}
data.requireUserAction(
type = UserActionRequiredEvent.Type.RECAPTCHA,
params = Bundle(
"siteKey" to siteKey,
"referer" to response.request().url().toString(),
"userAgent" to LIBRUS_USER_AGENT,
),
errorText = R.string.notification_user_action_required_captcha_librus,
)
return true
}
return false
}
private fun login(
url: String,
referer: String,
csrfToken: String?,
params: Map<String, String>,
) {
if (loginPerformed) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR))
return
}
d(TAG, "Request: Librus/Login/Portal - $url")
val recaptchaCode = data.arguments?.getString("recaptchaCode") ?: data.loginStore.getLoginData("recaptchaCode", null) val recaptchaCode = data.arguments?.getString("recaptchaCode") ?: data.loginStore.getLoginData("recaptchaCode", null)
val recaptchaTime = data.arguments?.getLong("recaptchaTime") ?: data.loginStore.getLoginData("recaptchaTime", 0L) val recaptchaTime = data.arguments?.getLong("recaptchaTime") ?: data.loginStore.getLoginData("recaptchaTime", 0L)
@ -116,67 +186,46 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
Request.builder() Request.builder()
.url(if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL) .url(if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL)
.userAgent(LIBRUS_USER_AGENT) .userAgent(LIBRUS_USER_AGENT)
.addHeader("X-Requested-With", LIBRUS_HEADER)
.addHeader("Referer", referer)
.withClient(data.app.httpLazy)
.addParameter("email", data.portalEmail) .addParameter("email", data.portalEmail)
.addParameter("password", data.portalPassword) .addParameter("password", data.portalPassword)
.also { .also {
if (recaptchaCode != null && System.currentTimeMillis() - recaptchaTime < 2*60*1000 /* 2 minutes */) if (recaptchaCode != null && System.currentTimeMillis() - recaptchaTime < 2*60*1000 /* 2 minutes */)
it.addParameter("g-recaptcha-response", recaptchaCode) it.addParameter("g-recaptcha-response", recaptchaCode)
if (csrfToken != null)
it.addHeader("X-CSRF-TOKEN", csrfToken)
for ((key, value) in params) {
it.addParameter(key, value)
}
} }
.addHeader("X-CSRF-TOKEN", csrfToken) .contentType(MediaTypeUtils.APPLICATION_FORM)
.allowErrorCode(HTTP_BAD_REQUEST)
.allowErrorCode(HTTP_FORBIDDEN)
.contentType(MediaTypeUtils.APPLICATION_JSON)
.post() .post()
.callback(object : JsonCallbackHandler() { .callback(object : TextCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response) { override fun onSuccess(text: String?, response: Response) {
loginPerformed = true
val location = response.headers()?.get("Location") val location = response.headers()?.get("Location")
if (location == "$LIBRUS_REDIRECT_URL?command=close") { if (location == "$LIBRUS_REDIRECT_URL?command=close") {
data.error(ApiError(TAG, ERROR_LIBRUS_PORTAL_MAINTENANCE) data.error(ApiError(TAG, ERROR_LIBRUS_PORTAL_MAINTENANCE)
.withApiResponse(json) .withApiResponse(text)
.withResponse(response)) .withResponse(response))
return return
} }
if (text == null) {
if (json == null) {
if (response.parserErrorBody?.contains("wciąż nieaktywne") == true) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED)
.withResponse(response))
return
}
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response)) .withResponse(response))
return return
} }
val error = if (response.code() == 200) null else
json.getJsonArray("errors")?.getString(0)
?: json.getJsonObject("errors")?.entrySet()?.firstOrNull()?.value?.asString
if (error?.contains("robotem") == true || json.getBoolean("captchaRequired") == true) { authorize(
data.requireUserAction( url = location
type = UserActionRequiredEvent.Type.RECAPTCHA, ?: if (data.fakeLogin)
params = Bundle( FAKE_LIBRUS_AUTHORIZE
"siteKey" to LIBRUS_PORTAL_RECAPTCHA_KEY, else
"referer" to LIBRUS_PORTAL_RECAPTCHA_REFERER, LIBRUS_AUTHORIZE_URL,
), referer = referer,
errorText = R.string.notification_user_action_required_captcha_librus, )
)
return
}
error?.let { code ->
when {
code.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED
code.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
code.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
else -> ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR
}.let { errorCode ->
data.error(ApiError(TAG, errorCode)
.withApiResponse(json)
.withResponse(response))
return
}
}
authorize(json.getString("redirect", LIBRUS_AUTHORIZE_URL))
} }
override fun onFailure(response: Response, throwable: Throwable) { override fun onFailure(response: Response, throwable: Throwable) {

View File

@ -11,7 +11,9 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod
import pl.szczodrzynski.edziennik.ext.currentTimeUnix import pl.szczodrzynski.edziennik.ext.currentTimeUnix
import pl.szczodrzynski.edziennik.ext.getStudentData
import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.ext.set
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
@ -85,7 +87,7 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da
private var mStudentId: Int? = null private var mStudentId: Int? = null
var studentId: Int var studentId: Int
get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 } get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 }
set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } set(value) { profile["studentId"] = value; mStudentId = value }
/* __ __ _ /* __ __ _
\ \ / / | | \ \ / / | |
@ -125,7 +127,7 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da
*/ */
var globalId: String? var globalId: String?
get() { mGlobalId = mGlobalId ?: profile?.getStudentData("globalId", null); return mGlobalId } get() { mGlobalId = mGlobalId ?: profile?.getStudentData("globalId", null); return mGlobalId }
set(value) { profile?.putStudentData("globalId", value) ?: return; mGlobalId = value } set(value) { profile["globalId"] = value; mGlobalId = value }
private var mGlobalId: String? = null private var mGlobalId: String? = null
/** /**
@ -135,7 +137,7 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da
*/ */
var loginEmail: String? var loginEmail: String?
get() { mLoginEmail = mLoginEmail ?: profile?.getStudentData("email", null); return mLoginEmail } get() { mLoginEmail = mLoginEmail ?: profile?.getStudentData("email", null); return mLoginEmail }
set(value) { profile?.putStudentData("email", value); mLoginEmail = value } set(value) { profile["email"] = value; mLoginEmail = value }
private var mLoginEmail: String? = null private var mLoginEmail: String? = null
/** /**
@ -144,7 +146,7 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da
*/ */
var loginId: String? var loginId: String?
get() { mLoginId = mLoginId ?: profile?.getStudentData("loginId", null); return mLoginId } get() { mLoginId = mLoginId ?: profile?.getStudentData("loginId", null); return mLoginId }
set(value) { profile?.putStudentData("loginId", value) ?: return; mLoginId = value } set(value) { profile["loginId"] = value; mLoginId = value }
private var mLoginId: String? = null private var mLoginId: String? = null
/** /**
@ -152,7 +154,7 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da
*/ */
var ciasteczkoAutoryzacji: String? var ciasteczkoAutoryzacji: String?
get() { mCiasteczkoAutoryzacji = mCiasteczkoAutoryzacji ?: profile?.getStudentData("ciasteczkoAutoryzacji", null); return mCiasteczkoAutoryzacji } get() { mCiasteczkoAutoryzacji = mCiasteczkoAutoryzacji ?: profile?.getStudentData("ciasteczkoAutoryzacji", null); return mCiasteczkoAutoryzacji }
set(value) { profile?.putStudentData("ciasteczkoAutoryzacji", value) ?: return; mCiasteczkoAutoryzacji = value } set(value) { profile["ciasteczkoAutoryzacji"] = value; mCiasteczkoAutoryzacji = value }
private var mCiasteczkoAutoryzacji: String? = null private var mCiasteczkoAutoryzacji: String? = null

View File

@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Attendance.Companion.TYPE_PRESE
import pl.szczodrzynski.edziennik.data.db.entity.Attendance.Companion.TYPE_RELEASED import pl.szczodrzynski.edziennik.data.db.entity.Attendance.Companion.TYPE_RELEASED
import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
import pl.szczodrzynski.edziennik.ext.dateToSemester
class MobidziennikApiAttendance(val data: DataMobidziennik, rows: List<String>) { class MobidziennikApiAttendance(val data: DataMobidziennik, rows: List<String>) {
init { run { init { run {

View File

@ -88,6 +88,7 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List<String>) {
} }
it.id = it.buildId() it.id = it.buildId()
it.ownerId = it.buildOwnerId()
val seen = profile.empty || date < Date.getToday() val seen = profile.empty || date < Date.getToday()

View File

@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBID
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb
import pl.szczodrzynski.edziennik.ext.DAY import pl.szczodrzynski.edziennik.ext.DAY
import pl.szczodrzynski.edziennik.ext.get import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.ext.isNotNullNorBlank
class MobidziennikWebAccountEmail(override val data: DataMobidziennik, class MobidziennikWebAccountEmail(override val data: DataMobidziennik,
override val lastSync: Long?, override val lastSync: Long?,
@ -24,7 +25,8 @@ class MobidziennikWebAccountEmail(override val data: DataMobidziennik,
MobidziennikLuckyNumberExtractor(data, text) MobidziennikLuckyNumberExtractor(data, text)
val email = Regexes.MOBIDZIENNIK_ACCOUNT_EMAIL.find(text)?.let { it[1] } val email = Regexes.MOBIDZIENNIK_ACCOUNT_EMAIL.find(text)?.let { it[1] }
data.loginEmail = email if (email.isNotNullNorBlank())
data.loginEmail = email
data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL, if (email == null) 3* DAY else 7* DAY) data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL, if (email == null) 3* DAY else 7* DAY)
onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL) onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL)

View File

@ -21,6 +21,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Attendance.Companion.TYPE_UNKNO
import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
import pl.szczodrzynski.edziennik.ext.dateToSemester
import pl.szczodrzynski.edziennik.ext.fixName import pl.szczodrzynski.edziennik.ext.fixName
import pl.szczodrzynski.edziennik.ext.get import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.ext.singleOrNull import pl.szczodrzynski.edziennik.ext.singleOrNull

View File

@ -15,6 +15,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NORMAL
import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
import pl.szczodrzynski.edziennik.ext.dateToSemester
import pl.szczodrzynski.edziennik.ext.fixWhiteSpaces import pl.szczodrzynski.edziennik.ext.fixWhiteSpaces
import pl.szczodrzynski.edziennik.ext.get import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.ext.singleOrNull import pl.szczodrzynski.edziennik.ext.singleOrNull

View File

@ -336,6 +336,7 @@ class MobidziennikWebTimetable(
} }
it.id = it.buildId() it.id = it.buildId()
it.ownerId = it.buildOwnerId()
it.isExtra = isExtra it.isExtra = isExtra
val seen = profile?.empty == false || lessonDate < Date.getToday() val seen = profile?.empty == false || lessonDate < Date.getToday()

View File

@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.ext.JsonObject import pl.szczodrzynski.edziennik.ext.JsonObject
import pl.szczodrzynski.edziennik.ext.getJsonObject import pl.szczodrzynski.edziennik.ext.getJsonObject
import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.ext.getString
import pl.szczodrzynski.edziennik.ext.isNotNullNorBlank
import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils
@ -77,7 +78,9 @@ class MobidziennikLoginApi2(val data: DataMobidziennik, val onSuccess: () -> Uni
} }
} }
data.loginEmail = json.getString("email") val email = json.getString("email")
if (email.isNotNullNorBlank())
data.loginEmail = email
data.globalId = json.getString("id_global") data.globalId = json.getString("id_global")
data.loginId = json.getString("login") data.loginId = json.getString("login")
onSuccess() onSuccess()

View File

@ -10,7 +10,9 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod
import pl.szczodrzynski.edziennik.ext.crc32 import pl.szczodrzynski.edziennik.ext.crc32
import pl.szczodrzynski.edziennik.ext.getStudentData
import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.ext.set
class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
@ -40,7 +42,7 @@ class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(a
private var mApiUrl: String? = null private var mApiUrl: String? = null
var apiUrl: String? var apiUrl: String?
get() { mApiUrl = mApiUrl ?: profile?.getStudentData("apiUrl", null); return mApiUrl } get() { mApiUrl = mApiUrl ?: profile?.getStudentData("apiUrl", null); return mApiUrl }
set(value) { profile?.putStudentData("apiUrl", value) ?: return; mApiUrl = value } set(value) { profile["apiUrl"] = value; mApiUrl = value }
/* ____ _ _ /* ____ _ _
/ __ \| | | | / __ \| | | |
@ -51,32 +53,32 @@ class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(a
private var mStudentId: String? = null private var mStudentId: String? = null
var studentId: String? var studentId: String?
get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", null); return mStudentId } get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", null); return mStudentId }
set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } set(value) { profile["studentId"] = value; mStudentId = value }
private var mStudentLogin: String? = null private var mStudentLogin: String? = null
var studentLogin: String? var studentLogin: String?
get() { mStudentLogin = mStudentLogin ?: profile?.getStudentData("studentLogin", null); return mStudentLogin } get() { mStudentLogin = mStudentLogin ?: profile?.getStudentData("studentLogin", null); return mStudentLogin }
set(value) { profile?.putStudentData("studentLogin", value) ?: return; mStudentLogin = value } set(value) { profile["studentLogin"] = value; mStudentLogin = value }
private var mSchoolName: String? = null private var mSchoolName: String? = null
var schoolName: String? var schoolName: String?
get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName } get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName }
set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolName = value } set(value) { profile["schoolName"] = value; mSchoolName = value }
private var mClassName: String? = null private var mClassName: String? = null
var className: String? var className: String?
get() { mClassName = mClassName ?: profile?.getStudentData("className", null); return mClassName } get() { mClassName = mClassName ?: profile?.getStudentData("className", null); return mClassName }
set(value) { profile?.putStudentData("className", value) ?: return; mClassName = value } set(value) { profile["className"] = value; mClassName = value }
private var mSchoolYear: String? = null private var mSchoolYear: String? = null
var schoolYear: String? var schoolYear: String?
get() { mSchoolYear = mSchoolYear ?: profile?.getStudentData("schoolYear", null); return mSchoolYear } get() { mSchoolYear = mSchoolYear ?: profile?.getStudentData("schoolYear", null); return mSchoolYear }
set(value) { profile?.putStudentData("schoolYear", value) ?: return; mSchoolYear = value } set(value) { profile["schoolYear"] = value; mSchoolYear = value }
private var mCurrentSemester: Int? = null private var mCurrentSemester: Int? = null
var currentSemester: Int var currentSemester: Int
get() { mCurrentSemester = mCurrentSemester ?: profile?.getStudentData("currentSemester", 0); return mCurrentSemester ?: 0 } get() { mCurrentSemester = mCurrentSemester ?: profile?.getStudentData("currentSemester", 0); return mCurrentSemester ?: 0 }
set(value) { profile?.putStudentData("currentSemester", value) ?: return; mCurrentSemester = value } set(value) { profile["currentSemester"] = value; mCurrentSemester = value }
val schoolShortName: String? val schoolShortName: String?
get() = studentLogin?.split('@')?.get(1)?.replace(".podlaskie.pl", "") get() = studentLogin?.split('@')?.get(1)?.replace(".podlaskie.pl", "")

View File

@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_PROPO
import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
import pl.szczodrzynski.edziennik.ext.getLong import pl.szczodrzynski.edziennik.ext.getLong
import pl.szczodrzynski.edziennik.ext.getSemesterStart
import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.ext.getString
class PodlasieApiFinalGrades(val data: DataPodlasie, val rows: List<JsonObject>) { class PodlasieApiFinalGrades(val data: DataPodlasie, val rows: List<JsonObject>) {

View File

@ -72,6 +72,7 @@ class PodlasieApiTimetable(val data: DataPodlasie, rows: List<JsonObject>) {
it.classroom = classroom it.classroom = classroom
it.id = it.buildId() it.id = it.buildId()
it.ownerId = it.buildOwnerId()
data.lessonList += it data.lessonList += it
} }
} }

View File

@ -10,7 +10,9 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod
import pl.szczodrzynski.edziennik.ext.currentTimeUnix import pl.szczodrzynski.edziennik.ext.currentTimeUnix
import pl.szczodrzynski.edziennik.ext.getStudentData
import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.ext.set
/** /**
* Use http://patorjk.com/software/taag/#p=display&f=Big for the ascii art * Use http://patorjk.com/software/taag/#p=display&f=Big for the ascii art
@ -43,12 +45,12 @@ class DataTemplate(app: App, profile: Profile?, loginStore: LoginStore) : Data(a
private var mWebCookie: String? = null private var mWebCookie: String? = null
var webCookie: String? var webCookie: String?
get() { mWebCookie = mWebCookie ?: profile?.getStudentData("webCookie", null); return mWebCookie } get() { mWebCookie = mWebCookie ?: profile?.getStudentData("webCookie", null); return mWebCookie }
set(value) { profile?.putStudentData("webCookie", value) ?: return; mWebCookie = value } set(value) { profile["webCookie"] = value; mWebCookie = value }
private var mWebExpiryTime: Long? = null private var mWebExpiryTime: Long? = null
var webExpiryTime: Long var webExpiryTime: Long
get() { mWebExpiryTime = mWebExpiryTime ?: profile?.getStudentData("webExpiryTime", 0L); return mWebExpiryTime ?: 0L } get() { mWebExpiryTime = mWebExpiryTime ?: profile?.getStudentData("webExpiryTime", 0L); return mWebExpiryTime ?: 0L }
set(value) { profile?.putStudentData("webExpiryTime", value) ?: return; mWebExpiryTime = value } set(value) { profile["webExpiryTime"] = value; mWebExpiryTime = value }
/* _ /* _
/\ (_) /\ (_)
@ -61,10 +63,10 @@ class DataTemplate(app: App, profile: Profile?, loginStore: LoginStore) : Data(a
private var mApiToken: String? = null private var mApiToken: String? = null
var apiToken: String? var apiToken: String?
get() { mApiToken = mApiToken ?: profile?.getStudentData("apiToken", null); return mApiToken } get() { mApiToken = mApiToken ?: profile?.getStudentData("apiToken", null); return mApiToken }
set(value) { profile?.putStudentData("apiToken", value) ?: return; mApiToken = value } set(value) { profile["apiToken"] = value; mApiToken = value }
private var mApiExpiryTime: Long? = null private var mApiExpiryTime: Long? = null
var apiExpiryTime: Long var apiExpiryTime: Long
get() { mApiExpiryTime = mApiExpiryTime ?: profile?.getStudentData("apiExpiryTime", 0L); return mApiExpiryTime ?: 0L } get() { mApiExpiryTime = mApiExpiryTime ?: profile?.getStudentData("apiExpiryTime", 0L); return mApiExpiryTime ?: 0L }
set(value) { profile?.putStudentData("apiExpiryTime", value) ?: return; mApiExpiryTime = value } set(value) { profile["apiExpiryTime"] = value; mApiExpiryTime = value }
} }

View File

@ -9,6 +9,8 @@ import pl.szczodrzynski.edziennik.data.api.models.Data
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod
import pl.szczodrzynski.edziennik.ext.getStudentData
import pl.szczodrzynski.edziennik.ext.set
class DataUsos( class DataUsos(
app: App, app: App,
@ -69,6 +71,6 @@ class DataUsos(
var studentId: Int var studentId: Int
get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 } get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 }
set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } set(value) { profile["studentId"] = value; mStudentId = value }
private var mStudentId: Int? = null private var mStudentId: Int? = null
} }

View File

@ -41,24 +41,18 @@ class UsosApiTerms(
} }
private fun processResponse(json: JsonArray): Boolean { private fun processResponse(json: JsonArray): Boolean {
val dates = mutableSetOf<Date>() val today = Date.getToday()
for (term in json.asJsonObjectList()) { for (term in json.asJsonObjectList()) {
if (!term.getBoolean("is_active", false)) if (!term.getBoolean("is_active", false))
continue continue
val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) } val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) } ?: continue
val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) } val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) } ?: continue
if (startDate != null) if (today in startDate..finishDate) {
dates += startDate profile?.studentSchoolYearStart = startDate.year
if (finishDate != null) profile?.dateSemester1Start = startDate
dates += finishDate profile?.dateSemester2Start = finishDate
}
} }
val datesSorted = dates.sorted()
if (datesSorted.size != 3)
return false
profile?.studentSchoolYearStart = datesSorted[0].year
profile?.dateSemester1Start = datesSorted[0]
profile?.dateSemester2Start = datesSorted[1]
profile?.dateYearEnd = datesSorted[2]
return true return true
} }
} }

View File

@ -104,8 +104,9 @@ class UsosApiTimetable(
it.teacherId = lecturerIds?.firstOrNull() ?: -1L it.teacherId = lecturerIds?.firstOrNull() ?: -1L
it.teamId = unitId it.teamId = unitId
val groupName = classTypeId?.plus(groupNumber)?.let { s -> "($s)" } val groupName = classTypeId?.plus(groupNumber)?.let { s -> "($s)" }
it.classroom = "$buildingId / $roomNumber ${groupName ?: ""}" it.classroom = "Sala $roomNumber / bud. $buildingId ${groupName ?: ""}"
it.id = it.buildId() it.id = it.buildId()
it.ownerId = it.buildOwnerId()
it.color = when (classTypeId) { it.color = when (classTypeId) {
"WYK" -> 0xff0d6091 "WYK" -> 0xff0d6091

View File

@ -12,7 +12,9 @@ import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod
import pl.szczodrzynski.edziennik.ext.crc16 import pl.szczodrzynski.edziennik.ext.crc16
import pl.szczodrzynski.edziennik.ext.currentTimeUnix import pl.szczodrzynski.edziennik.ext.currentTimeUnix
import pl.szczodrzynski.edziennik.ext.getStudentData
import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.ext.set
import pl.szczodrzynski.fslogin.realm.RealmData import pl.szczodrzynski.fslogin.realm.RealmData
class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) {
@ -57,7 +59,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mSymbol: String? = null private var mSymbol: String? = null
var symbol: String? var symbol: String?
get() { mSymbol = mSymbol ?: profile?.getStudentData("symbol", null); return mSymbol } get() { mSymbol = mSymbol ?: profile?.getStudentData("symbol", null); return mSymbol }
set(value) { profile?.putStudentData("symbol", value); mSymbol = value } set(value) { profile["symbol"] = value; mSymbol = value }
/** /**
* Group symbol/number of the student's school. * Group symbol/number of the student's school.
@ -69,7 +71,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mSchoolSymbol: String? = null private var mSchoolSymbol: String? = null
var schoolSymbol: String? var schoolSymbol: String?
get() { mSchoolSymbol = mSchoolSymbol ?: profile?.getStudentData("schoolSymbol", null); return mSchoolSymbol } get() { mSchoolSymbol = mSchoolSymbol ?: profile?.getStudentData("schoolSymbol", null); return mSchoolSymbol }
set(value) { profile?.putStudentData("schoolSymbol", value) ?: return; mSchoolSymbol = value } set(value) { profile["schoolSymbol"] = value; mSchoolSymbol = value }
/** /**
* Short name of the school, used in some places. * Short name of the school, used in some places.
@ -79,7 +81,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mSchoolShort: String? = null private var mSchoolShort: String? = null
var schoolShort: String? var schoolShort: String?
get() { mSchoolShort = mSchoolShort ?: profile?.getStudentData("schoolShort", null); return mSchoolShort } get() { mSchoolShort = mSchoolShort ?: profile?.getStudentData("schoolShort", null); return mSchoolShort }
set(value) { profile?.putStudentData("schoolShort", value) ?: return; mSchoolShort = value } set(value) { profile["schoolShort"] = value; mSchoolShort = value }
/** /**
* A school code consisting of the [symbol] and [schoolSymbol]. * A school code consisting of the [symbol] and [schoolSymbol].
@ -91,7 +93,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mSchoolCode: String? = null private var mSchoolCode: String? = null
var schoolCode: String? var schoolCode: String?
get() { mSchoolCode = mSchoolCode ?: profile?.getStudentData("schoolName", null); return mSchoolCode } get() { mSchoolCode = mSchoolCode ?: profile?.getStudentData("schoolName", null); return mSchoolCode }
set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolCode = value } set(value) { profile["schoolName"] = value; mSchoolCode = value }
/** /**
* ID of the student. * ID of the student.
@ -101,7 +103,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mStudentId: Int? = null private var mStudentId: Int? = null
var studentId: Int var studentId: Int
get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 } get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 }
set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } set(value) { profile["studentId"] = value; mStudentId = value }
/** /**
* ID of the student's account. * ID of the student's account.
@ -111,7 +113,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mStudentLoginId: Int? = null private var mStudentLoginId: Int? = null
var studentLoginId: Int var studentLoginId: Int
get() { mStudentLoginId = mStudentLoginId ?: profile?.getStudentData("studentLoginId", 0); return mStudentLoginId ?: 0 } get() { mStudentLoginId = mStudentLoginId ?: profile?.getStudentData("studentLoginId", 0); return mStudentLoginId ?: 0 }
set(value) { profile?.putStudentData("studentLoginId", value) ?: return; mStudentLoginId = value } set(value) { profile["studentLoginId"] = value; mStudentLoginId = value }
/** /**
* ID of the student's class. * ID of the student's class.
@ -121,7 +123,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mStudentClassId: Int? = null private var mStudentClassId: Int? = null
var studentClassId: Int var studentClassId: Int
get() { mStudentClassId = mStudentClassId ?: profile?.getStudentData("studentClassId", 0); return mStudentClassId ?: 0 } get() { mStudentClassId = mStudentClassId ?: profile?.getStudentData("studentClassId", 0); return mStudentClassId ?: 0 }
set(value) { profile?.putStudentData("studentClassId", value) ?: return; mStudentClassId = value } set(value) { profile["studentClassId"] = value; mStudentClassId = value }
/** /**
* ListaUczniow/IdOkresKlasyfikacyjny, e.g. 321 * ListaUczniow/IdOkresKlasyfikacyjny, e.g. 321
@ -129,26 +131,26 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mStudentSemesterId: Int? = null private var mStudentSemesterId: Int? = null
var studentSemesterId: Int var studentSemesterId: Int
get() { mStudentSemesterId = mStudentSemesterId ?: profile?.getStudentData("studentSemesterId", 0); return mStudentSemesterId ?: 0 } get() { mStudentSemesterId = mStudentSemesterId ?: profile?.getStudentData("studentSemesterId", 0); return mStudentSemesterId ?: 0 }
set(value) { profile?.putStudentData("studentSemesterId", value) ?: return; mStudentSemesterId = value } set(value) { profile["studentSemesterId"] = value; mStudentSemesterId = value }
private var mStudentUnitId: Int? = null private var mStudentUnitId: Int? = null
var studentUnitId: Int var studentUnitId: Int
get() { mStudentUnitId = mStudentUnitId ?: profile?.getStudentData("studentUnitId", 0); return mStudentUnitId ?: 0 } get() { mStudentUnitId = mStudentUnitId ?: profile?.getStudentData("studentUnitId", 0); return mStudentUnitId ?: 0 }
set(value) { profile?.putStudentData("studentUnitId", value) ?: return; mStudentUnitId = value } set(value) { profile["studentUnitId"] = value; mStudentUnitId = value }
private var mStudentConstituentId: Int? = null private var mStudentConstituentId: Int? = null
var studentConstituentId: Int var studentConstituentId: Int
get() { mStudentConstituentId = mStudentConstituentId ?: profile?.getStudentData("studentConstituentId", 0); return mStudentConstituentId ?: 0 } get() { mStudentConstituentId = mStudentConstituentId ?: profile?.getStudentData("studentConstituentId", 0); return mStudentConstituentId ?: 0 }
set(value) { profile?.putStudentData("studentConstituentId", value) ?: return; mStudentConstituentId = value } set(value) { profile["studentConstituentId"] = value; mStudentConstituentId = value }
private var mSemester1Id: Int? = null private var mSemester1Id: Int? = null
var semester1Id: Int var semester1Id: Int
get() { mSemester1Id = mSemester1Id ?: profile?.getStudentData("semester1Id", 0); return mSemester1Id ?: 0 } get() { mSemester1Id = mSemester1Id ?: profile?.getStudentData("semester1Id", 0); return mSemester1Id ?: 0 }
set(value) { profile?.putStudentData("semester1Id", value) ?: return; mSemester1Id = value } set(value) { profile["semester1Id"] = value; mSemester1Id = value }
private var mSemester2Id: Int? = null private var mSemester2Id: Int? = null
var semester2Id: Int var semester2Id: Int
get() { mSemester2Id = mSemester2Id ?: profile?.getStudentData("semester2Id", 0); return mSemester2Id ?: 0 } get() { mSemester2Id = mSemester2Id ?: profile?.getStudentData("semester2Id", 0); return mSemester2Id ?: 0 }
set(value) { profile?.putStudentData("semester2Id", value) ?: return; mSemester2Id = value } set(value) { profile["semester2Id"] = value; mSemester2Id = value }
/** /**
* ListaUczniow/OkresNumer, e.g. 1 or 2 * ListaUczniow/OkresNumer, e.g. 1 or 2
@ -156,7 +158,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mStudentSemesterNumber: Int? = null private var mStudentSemesterNumber: Int? = null
var studentSemesterNumber: Int var studentSemesterNumber: Int
get() { mStudentSemesterNumber = mStudentSemesterNumber ?: profile?.getStudentData("studentSemesterNumber", 0); return mStudentSemesterNumber ?: 0 } get() { mStudentSemesterNumber = mStudentSemesterNumber ?: profile?.getStudentData("studentSemesterNumber", 0); return mStudentSemesterNumber ?: 0 }
set(value) { profile?.putStudentData("studentSemesterNumber", value) ?: return; mStudentSemesterNumber = value } set(value) { profile["studentSemesterNumber"] = value; mStudentSemesterNumber = value }
/** /**
* Date of the end of the current semester ([studentSemesterNumber]). * Date of the end of the current semester ([studentSemesterNumber]).
@ -166,7 +168,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mCurrentSemesterEndDate: Long? = null private var mCurrentSemesterEndDate: Long? = null
var currentSemesterEndDate: Long var currentSemesterEndDate: Long
get() { mCurrentSemesterEndDate = mCurrentSemesterEndDate ?: profile?.getStudentData("currentSemesterEndDate", 0L); return mCurrentSemesterEndDate ?: 0L } get() { mCurrentSemesterEndDate = mCurrentSemesterEndDate ?: profile?.getStudentData("currentSemesterEndDate", 0L); return mCurrentSemesterEndDate ?: 0L }
set(value) { profile?.putStudentData("currentSemesterEndDate", value) ?: return; mCurrentSemesterEndDate = value } set(value) { profile["currentSemesterEndDate"] = value; mCurrentSemesterEndDate = value }
/* _____ _____ ____ /* _____ _____ ____
/\ | __ \_ _| |___ \ /\ | __ \_ _| |___ \
@ -219,17 +221,17 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app
private var mHebeContext: String? = null private var mHebeContext: String? = null
var hebeContext: String? var hebeContext: String?
get() { mHebeContext = mHebeContext ?: profile?.getStudentData("hebeContext", null); return mHebeContext } get() { mHebeContext = mHebeContext ?: profile?.getStudentData("hebeContext", null); return mHebeContext }
set(value) { profile?.putStudentData("hebeContext", value) ?: return; mHebeContext = value } set(value) { profile["hebeContext"] = value; mHebeContext = value }
private var mMessageBoxKey: String? = null private var mMessageBoxKey: String? = null
var messageBoxKey: String? var messageBoxKey: String?
get() { mMessageBoxKey = mMessageBoxKey ?: profile?.getStudentData("messageBoxKey", null); return mMessageBoxKey } get() { mMessageBoxKey = mMessageBoxKey ?: profile?.getStudentData("messageBoxKey", null); return mMessageBoxKey }
set(value) { profile?.putStudentData("messageBoxKey", value) ?: return; mMessageBoxKey = value } set(value) { profile["messageBoxKey"] = value; mMessageBoxKey = value }
private var mMessageBoxName: String? = null private var mMessageBoxName: String? = null
var messageBoxName: String? var messageBoxName: String?
get() { mMessageBoxName = mMessageBoxName ?: profile?.getStudentData("messageBoxName", null); return mMessageBoxName } get() { mMessageBoxName = mMessageBoxName ?: profile?.getStudentData("messageBoxName", null); return mMessageBoxName }
set(value) { profile?.putStudentData("messageBoxName", value) ?: return; mMessageBoxName = value } set(value) { profile["messageBoxName"] = value; mMessageBoxName = value }
val apiUrl: String? val apiUrl: String?
get() { get() {

View File

@ -26,6 +26,7 @@ import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.HttpURLConnection.HTTP_NOT_FOUND
import java.net.URLEncoder import java.net.URLEncoder
import java.time.Instant import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
@ -183,6 +184,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
payload: JsonElement? = null, payload: JsonElement? = null,
baseUrl: Boolean = false, baseUrl: Boolean = false,
firebaseToken: String? = null, firebaseToken: String? = null,
allow404: Boolean = false,
crossinline onSuccess: (json: T, response: Response?) -> Unit crossinline onSuccess: (json: T, response: Response?) -> Unit
) { ) {
val url = "${if (baseUrl) data.apiUrl else data.fullApiUrl}$endpoint" val url = "${if (baseUrl) data.apiUrl else data.fullApiUrl}$endpoint"
@ -295,6 +297,19 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
} }
override fun onFailure(response: Response?, throwable: Throwable?) { override fun onFailure(response: Response?, throwable: Throwable?) {
if (allow404 && response?.code() == HTTP_NOT_FOUND) {
try {
onSuccess(null as T, response)
} catch (e: Exception) {
data.error(
ApiError(tag, EXCEPTION_VULCAN_HEBE_REQUEST)
.withResponse(response)
.withThrowable(e)
)
}
return
}
data.error( data.error(
ApiError(tag, ERROR_REQUEST_FAILURE) ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response) .withResponse(response)
@ -338,6 +353,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
query: Map<String, String> = mapOf(), query: Map<String, String> = mapOf(),
baseUrl: Boolean = false, baseUrl: Boolean = false,
firebaseToken: String? = null, firebaseToken: String? = null,
allow404: Boolean = false,
crossinline onSuccess: (json: T, response: Response?) -> Unit crossinline onSuccess: (json: T, response: Response?) -> Unit
) { ) {
val queryPath = query.map { val queryPath = query.map {
@ -348,6 +364,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
if (query.isNotEmpty()) "$endpoint?$queryPath" else endpoint, if (query.isNotEmpty()) "$endpoint?$queryPath" else endpoint,
baseUrl = baseUrl, baseUrl = baseUrl,
firebaseToken = firebaseToken, firebaseToken = firebaseToken,
allow404 = allow404,
onSuccess = onSuccess onSuccess = onSuccess
) )
} }
@ -382,6 +399,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
messageBox: String? = null, messageBox: String? = null,
params: Map<String, String> = mapOf(), params: Map<String, String> = mapOf(),
includeFilterType: Boolean = true, includeFilterType: Boolean = true,
allow404: Boolean = false,
onSuccess: (data: List<JsonObject>, response: Response?) -> Unit onSuccess: (data: List<JsonObject>, response: Response?) -> Unit
) { ) {
val url = if (includeFilterType && filterType != null) val url = if (includeFilterType && filterType != null)
@ -427,8 +445,8 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
) )
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
apiGet(tag, url, query) { json: JsonArray, response -> apiGet(tag, url, query, allow404 = allow404) { json: JsonArray?, response ->
onSuccess(json.map { it.asJsonObject }, response) onSuccess(json?.map { it.asJsonObject } ?: listOf(), response)
} }
} }
} }

View File

@ -19,6 +19,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_PARENTS_
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_STUDENT import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_STUDENT
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_TEACHER import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_TEACHER
import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ext.*
import java.net.HttpURLConnection.HTTP_NOT_FOUND
class VulcanHebeAddressbook( class VulcanHebeAddressbook(
override val data: DataVulcan, override val data: DataVulcan,
@ -41,8 +42,15 @@ class VulcanHebeAddressbook(
VULCAN_HEBE_ENDPOINT_ADDRESSBOOK, VULCAN_HEBE_ENDPOINT_ADDRESSBOOK,
HebeFilterType.BY_PERSON, HebeFilterType.BY_PERSON,
lastSync = lastSync, lastSync = lastSync,
includeFilterType = false includeFilterType = false,
) { list, _ -> allow404 = true,
) { list, response ->
if (response?.code() == HTTP_NOT_FOUND) {
data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK, 2 * DAY)
onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK)
return@apiGetList
}
list.forEach { person -> list.forEach { person ->
val id = person.getString("Id") ?: return@forEach val id = person.getString("Id") ?: return@forEach

View File

@ -247,7 +247,9 @@ class VulcanHebeTimetable(
} }
lessonObject.id = lessonObject.buildId() lessonObject.id = lessonObject.buildId()
lessonObject.ownerId = lessonObject.buildOwnerId()
lessonShift?.id = lessonShift?.buildId() ?: -1 lessonShift?.id = lessonShift?.buildId() ?: -1
lessonShift?.ownerId = lessonShift?.buildOwnerId() ?: -1
lessonList.add(lessonObject) lessonList.add(lessonObject)
lessonShift?.let { lessonList.add(it) } lessonShift?.let { lessonList.add(it) }

View File

@ -13,7 +13,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.ERROR_API_INVALID_SIGNATURE import pl.szczodrzynski.edziennik.data.api.ERROR_API_INVALID_SIGNATURE
import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter
@ -128,16 +127,10 @@ class SzkolnyApi(val app: App) : CoroutineScope {
response: Response<ApiResponse<T>>, response: Response<ApiResponse<T>>,
updateDeviceHash: Boolean = false, updateDeviceHash: Boolean = false,
): T { ): T {
app.config.update = response.body()?.update?.let { update -> response.body()?.update?.let { update ->
if (update.versionCode > BuildConfig.VERSION_CODE) { // do not process "null" update, as it might not mean there's no update
if (update.updateMandatory // do not notify; silently check and show the home update card
&& EventBus.getDefault().hasSubscriberForEvent(update::class.java)) { app.updateManager.process(update, notify = false)
EventBus.getDefault().postSticky(update)
}
update
}
else
null
} }
response.body()?.registerAvailability?.let { registerAvailability -> response.body()?.registerAvailability?.let { registerAvailability ->
@ -205,7 +198,6 @@ class SzkolnyApi(val app: App) : CoroutineScope {
val teams = app.db.teamDao().allNow val teams = app.db.teamDao().allNow
val users = profiles.mapNotNull { profile -> val users = profiles.mapNotNull { profile ->
val config = app.config.getFor(profile.id)
val user = ServerSyncRequest.User( val user = ServerSyncRequest.User(
profile.userCode, profile.userCode,
profile.studentNameLong, profile.studentNameLong,
@ -214,9 +206,9 @@ class SzkolnyApi(val app: App) : CoroutineScope {
teams.filter { it.profileId == profile.id }.map { it.code } teams.filter { it.profileId == profile.id }.map { it.code }
) )
val hash = user.toString().md5() val hash = user.toString().md5()
if (hash == config.hash) if (hash == profile.config.hash)
return@mapNotNull null return@mapNotNull null
return@mapNotNull user to config return@mapNotNull user to profile.config
} }
val response = api.serverSync(ServerSyncRequest( val response = api.serverSync(ServerSyncRequest(
@ -265,12 +257,10 @@ class SzkolnyApi(val app: App) : CoroutineScope {
seen = profile.empty seen = profile.empty
notified = profile.empty notified = profile.empty
if (profile.userCode == event.sharedBy) { sharedBy = if (profile.userCode == event.sharedBy)
sharedBy = "self" "self"
addedManually = true else
} else { eventSharedBy
sharedBy = eventSharedBy
}
} }
} }
} }
@ -334,11 +324,14 @@ class SzkolnyApi(val app: App) : CoroutineScope {
} }
@Throws(Exception::class) @Throws(Exception::class)
fun shareNote(note: Note) { fun shareNote(note: Note, teamId: Long? = null) {
val profile = app.db.profileDao().getByIdNow(note.profileId) val profile = app.db.profileDao().getByIdNow(note.profileId)
?: throw NullPointerException("Profile is not found") ?: throw NullPointerException("Profile is not found")
val team = app.db.teamDao().getClassNow(note.profileId) val team = if (teamId == null)
?: throw NullPointerException("TeamClass is not found") app.db.teamDao().getClassNow(note.profileId)
else
app.db.teamDao().getByIdNow(note.profileId, teamId)
team ?: throw NullPointerException("TeamClass is not found")
val response = api.shareNote(NoteShareRequest( val response = api.shareNote(NoteShareRequest(
deviceId = app.deviceId, deviceId = app.deviceId,
@ -431,8 +424,8 @@ class SzkolnyApi(val app: App) : CoroutineScope {
} }
@Throws(Exception::class) @Throws(Exception::class)
fun getUpdate(channel: String): List<Update> { fun getUpdate(channel: Update.Type): List<Update> {
val response = api.updates(channel).execute() val response = api.updates(channel.name.lowercase()).execute()
return parseResponse(response) return parseResponse(response)
} }

View File

@ -12,10 +12,11 @@ import pl.szczodrzynski.edziennik.ext.bodyToString
import pl.szczodrzynski.edziennik.ext.currentTimeUnix import pl.szczodrzynski.edziennik.ext.currentTimeUnix
import pl.szczodrzynski.edziennik.ext.hmacSHA1 import pl.szczodrzynski.edziennik.ext.hmacSHA1
import pl.szczodrzynski.edziennik.ext.md5 import pl.szczodrzynski.edziennik.ext.md5
import pl.szczodrzynski.edziennik.ext.takeValue
class SignatureInterceptor(val app: App) : Interceptor { class SignatureInterceptor(val app: App) : Interceptor {
companion object { companion object {
private const val API_KEY = "szkolny_android_42a66f0842fc7da4e37c66732acf705a" const val API_KEY = "szkolny_android_42a66f0842fc7da4e37c66732acf705a"
} }
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
@ -27,12 +28,13 @@ class SignatureInterceptor(val app: App) : Interceptor {
return chain.proceed( return chain.proceed(
request.newBuilder() request.newBuilder()
.header("X-ApiKey", API_KEY) .header("X-ApiKey", app.config.apiKeyCustom?.takeValue() ?: API_KEY)
.header("X-AppVersion", BuildConfig.VERSION_CODE.toString())
.header("X-Timestamp", timestamp.toString())
.header("X-Signature", sign(timestamp, body, url))
.header("X-AppBuild", BuildConfig.BUILD_TYPE) .header("X-AppBuild", BuildConfig.BUILD_TYPE)
.header("X-AppFlavor", BuildConfig.FLAVOR) .header("X-AppFlavor", BuildConfig.FLAVOR)
.header("X-AppVersion", BuildConfig.VERSION_CODE.toString())
.header("X-DeviceId", app.deviceId)
.header("X-Signature", sign(timestamp, body, url))
.header("X-Timestamp", timestamp.toString())
.build()) .build())
} }

View File

@ -46,6 +46,6 @@ object Signing {
/*fun provideKey(param1: String, param2: Long): ByteArray {*/ /*fun provideKey(param1: String, param2: Long): ByteArray {*/
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
return "$param1.MTIzNDU2Nzg5MDoKyw75d3===.$param2".sha256() return "$param1.MTIzNDU2Nzg5MD0WAYwfGc===.$param2".sha256()
} }
} }

View File

@ -5,12 +5,21 @@
package pl.szczodrzynski.edziennik.data.api.szkolny.response package pl.szczodrzynski.edziennik.data.api.szkolny.response
data class Update( data class Update(
val versionCode: Int, val versionCode: Int,
val versionName: String, val versionName: String,
val releaseDate: String, val releaseDate: String,
val releaseNotes: String?, val releaseNotes: String?,
val releaseType: String, val releaseType: String,
val isOnGooglePlay: Boolean, val isOnGooglePlay: Boolean,
val downloadUrl: String?, val downloadUrl: String?,
val updateMandatory: Boolean val updateMandatory: Boolean,
) ) {
enum class Type {
NIGHTLY,
DEV,
BETA,
RC,
RELEASE,
}
}

View File

@ -55,7 +55,7 @@ class SzkolnyTask(val app: App, val syncingProfiles: List<Profile>) : IApiTask(-
notificationList notificationList
.mapNotNull { it.profileId } .mapNotNull { it.profileId }
.distinct() .distinct()
.map { app.config.getFor(it).sync.notificationFilter } .map { app.config[it].sync.notificationFilter }
.forEach { filter -> .forEach { filter ->
filter.forEach { type -> filter.forEach { type ->
notificationList.removeAll { it.type == type } notificationList.removeAll { it.type == type }

View File

@ -44,7 +44,7 @@ import pl.szczodrzynski.edziennik.data.db.migration.*
TimetableManual::class, TimetableManual::class,
Note::class, Note::class,
Metadata::class Metadata::class
], version = 99) ], version = 100)
@TypeConverters( @TypeConverters(
ConverterTime::class, ConverterTime::class,
ConverterDate::class, ConverterDate::class,
@ -188,6 +188,7 @@ abstract class AppDb : RoomDatabase() {
Migration97(), Migration97(),
Migration98(), Migration98(),
Migration99(), Migration99(),
Migration100(),
).allowMainThreadQueries().build() ).allowMainThreadQueries().build()
} }
} }

View File

@ -5,13 +5,37 @@
package pl.szczodrzynski.edziennik.data.db.converter package pl.szczodrzynski.edziennik.data.db.converter
import androidx.room.TypeConverter import androidx.room.TypeConverter
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod
import pl.szczodrzynski.edziennik.data.db.enums.LoginMode
import pl.szczodrzynski.edziennik.data.db.enums.LoginType
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
import pl.szczodrzynski.edziennik.data.db.enums.NotificationType
import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
class ConverterEnums { class ConverterEnums {
@TypeConverter @TypeConverter
fun fromEnum(value: Enum<*>?) = value?.toInt() fun fromFeatureType(value: FeatureType?) = value?.id
@TypeConverter
fun fromLoginMethod(value: LoginMethod?) = value?.id
@TypeConverter
fun fromLoginMode(value: LoginMode?) = value?.id
@TypeConverter
fun fromLoginType(value: LoginType?) = value?.id
@TypeConverter
fun fromMetadataType(value: MetadataType?) = value?.id
@TypeConverter
fun fromNotificationType(value: NotificationType?) = value?.id
@TypeConverter
fun fromNavTarget(value: NavTarget?) = value?.id
@TypeConverter @TypeConverter
fun toFeatureType(value: Int?) = value.asFeatureTypeOrNull() fun toFeatureType(value: Int?) = value.asFeatureTypeOrNull()

View File

@ -3,15 +3,16 @@
*/ */
package pl.szczodrzynski.edziennik.data.db.dao package pl.szczodrzynski.edziennik.data.db.dao
import android.content.Context import android.graphics.Color
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_DEFAULT import pl.szczodrzynski.edziennik.config.AppData
import pl.szczodrzynski.edziennik.data.db.entity.EventType import pl.szczodrzynski.edziennik.data.db.entity.EventType
import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT
import pl.szczodrzynski.edziennik.data.db.entity.Profile
@Dao @Dao
abstract class EventTypeDao { abstract class EventTypeDao {
@ -24,6 +25,9 @@ abstract class EventTypeDao {
@Query("DELETE FROM eventTypes WHERE profileId = :profileId") @Query("DELETE FROM eventTypes WHERE profileId = :profileId")
abstract fun clear(profileId: Int) abstract fun clear(profileId: Int)
@Query("DELETE FROM eventTypes WHERE profileId = :profileId AND eventTypeSource = :source")
abstract fun clearBySource(profileId: Int, source: Int)
@Query("SELECT * FROM eventTypes WHERE profileId = :profileId AND eventType = :typeId") @Query("SELECT * FROM eventTypes WHERE profileId = :profileId AND eventType = :typeId")
abstract fun getByIdNow(profileId: Int, typeId: Long): EventType? abstract fun getByIdNow(profileId: Int, typeId: Long): EventType?
@ -36,20 +40,37 @@ abstract class EventTypeDao {
@get:Query("SELECT * FROM eventTypes") @get:Query("SELECT * FROM eventTypes")
abstract val allNow: List<EventType> abstract val allNow: List<EventType>
fun addDefaultTypes(context: Context, profileId: Int): List<EventType> { fun addDefaultTypes(profile: Profile): List<EventType> {
val data = AppData.get(profile.loginStoreType)
var order = 100 var order = 100
val colorMap = EventType.getTypeColorMap() val typeList = data.eventTypes.map {
val typeList = EventType.getTypeNameMap().map { (id, name) ->
EventType( EventType(
profileId = profileId, profileId = profile.id,
id = id, id = it.id,
name = context.getString(name), name = it.name,
color = colorMap[id] ?: COLOR_DEFAULT, color = Color.parseColor(it.color),
order = order++, order = order++,
source = SOURCE_DEFAULT source = SOURCE_DEFAULT,
) )
} }
addAll(typeList) addAll(typeList)
return typeList return typeList
} }
fun getAllWithDefaults(profile: Profile): List<EventType> {
val eventTypes = getAllNow(profile.id)
val defaultIdsExpected = AppData.get(profile.loginStoreType).eventTypes
.map { it.id }
val defaultIdsFound = eventTypes.filter { it.source == SOURCE_DEFAULT }
.sortedBy { it.order }
.map { it.id }
if (defaultIdsExpected == defaultIdsFound)
return eventTypes
clearBySource(profile.id, SOURCE_DEFAULT)
addDefaultTypes(profile)
return eventTypes
}
} }

View File

@ -28,6 +28,9 @@ interface ProfileDao {
@Query("SELECT * FROM profiles WHERE profileId = :profileId") @Query("SELECT * FROM profiles WHERE profileId = :profileId")
fun getByIdNow(profileId: Int): Profile? fun getByIdNow(profileId: Int): Profile?
@Query("SELECT * FROM profiles WHERE profileId = :profileId")
suspend fun getByIdSuspend(profileId: Int): Profile?
@get:Query("SELECT * FROM profiles WHERE profileId >= 0 ORDER BY profileId") @get:Query("SELECT * FROM profiles WHERE profileId >= 0 ORDER BY profileId")
val all: LiveData<List<Profile>> val all: LiveData<List<Profile>>
@ -49,7 +52,7 @@ interface ProfileDao {
@get:Query("SELECT profileId FROM profiles WHERE profileId >= 0 ORDER BY profileId") @get:Query("SELECT profileId FROM profiles WHERE profileId >= 0 ORDER BY profileId")
val idsNow: List<Int> val idsNow: List<Int>
@Query("SELECT profiles.* FROM teams JOIN profiles USING(profileId) WHERE teamCode = :teamCode AND registration = " + Profile.REGISTRATION_ENABLED + " AND enableSharedEvents = 1") @Query("SELECT profiles.* FROM teams JOIN profiles USING(profileId) WHERE teamCode = :teamCode AND registration = " + Profile.REGISTRATION_ENABLED)
fun getByTeamCodeNowWithRegistration(teamCode: String?): List<Profile> fun getByTeamCodeNowWithRegistration(teamCode: String?): List<Profile>
@get:Query("SELECT profileId FROM profiles WHERE profileId > 0 ORDER BY profileId ASC LIMIT 1") @get:Query("SELECT profileId FROM profiles WHERE profileId > 0 ORDER BY profileId ASC LIMIT 1")

View File

@ -107,6 +107,9 @@ abstract class TimetableDao : BaseDao<Lesson, LessonFull> {
fun getByIdNow(profileId: Int, id: Long) = fun getByIdNow(profileId: Int, id: Long) =
getOneNow("$QUERY WHERE timetable.profileId = $profileId AND timetable.id = $id") getOneNow("$QUERY WHERE timetable.profileId = $profileId AND timetable.id = $id")
fun getByOwnerIdNow(profileId: Int, ownerId: Long) =
getOneNow("$QUERY WHERE timetable.profileId = $profileId AND timetable.ownerId = $ownerId")
@Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND isExtra = :isExtra AND type != -1 AND ((type != 3 AND date >= :dateFrom) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom))") @Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND isExtra = :isExtra AND type != -1 AND ((type != 3 AND date >= :dateFrom) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom))")
abstract fun dontKeepFromDate(profileId: Int, dateFrom: Date, isExtra: Boolean) abstract fun dontKeepFromDate(profileId: Int, dateFrom: Date, isExtra: Boolean)

View File

@ -73,13 +73,22 @@ open class Event(
const val COLOR_INFORMATION = 0xff039be5.toInt() const val COLOR_INFORMATION = 0xff039be5.toInt()
} }
/**
* Added manually - added by self, shared by self, or shared by someone else.
*/
@ColumnInfo(name = "eventAddedManually") @ColumnInfo(name = "eventAddedManually")
var addedManually: Boolean = false var addedManually: Boolean = false
get() = field || sharedBy == "self" get() = field || isShared
/**
* Shared by - user code who shared the event. Null if not shared.
* "Self" if shared by this app user.
*/
@ColumnInfo(name = "eventSharedBy") @ColumnInfo(name = "eventSharedBy")
var sharedBy: String? = null var sharedBy: String? = null
@ColumnInfo(name = "eventSharedByName") @ColumnInfo(name = "eventSharedByName")
var sharedByName: String? = null var sharedByName: String? = null
@ColumnInfo(name = "eventBlacklisted") @ColumnInfo(name = "eventBlacklisted")
var blacklisted: Boolean = false var blacklisted: Boolean = false
@ColumnInfo(name = "eventIsDone") @ColumnInfo(name = "eventIsDone")
@ -104,6 +113,27 @@ open class Event(
var attachmentIds: MutableList<Long>? = null var attachmentIds: MutableList<Long>? = null
var attachmentNames: MutableList<String>? = null var attachmentNames: MutableList<String>? = null
val isHomework
get() = type == TYPE_HOMEWORK
/**
* Whether the event is shared by anyone. Note that this implies [addedManually].
*/
val isShared
get() = sharedBy != null
/**
* Whether the event is shared by "self" (this app user).
*/
val isSharedSent
get() = sharedBy == "self"
/**
* Whether the event is shared by someone else from the class group.
*/
val isSharedReceived
get() = sharedBy != null && sharedBy != "self"
/** /**
* Add an attachment * Add an attachment
* @param id attachment ID * @param id attachment ID
@ -134,9 +164,6 @@ open class Event(
it.timeInMillis += 45 * MINUTE * 1000 it.timeInMillis += 45 * MINUTE * 1000
} }
val isHomework
get() = type == TYPE_HOMEWORK
@Ignore @Ignore
fun withMetadata(metadata: Metadata) = EventFull(this, metadata) fun withMetadata(metadata: Metadata) = EventFull(this, metadata)
} }

View File

@ -5,31 +5,6 @@ package pl.szczodrzynski.edziennik.data.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_CLASS_EVENT
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_DEFAULT
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ELEARNING
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ESSAY
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXAM
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXCURSION
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_HOMEWORK
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_INFORMATION
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PROJECT
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PT_MEETING
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_READING
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_SHORT_QUIZ
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_CLASS_EVENT
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_DEFAULT
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ELEARNING
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ESSAY
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXAM
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXCURSION
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_HOMEWORK
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PROJECT
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PT_MEETING
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_READING
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_SHORT_QUIZ
@Entity( @Entity(
tableName = "eventTypes", tableName = "eventTypes",
@ -55,35 +30,5 @@ class EventType(
const val SOURCE_REGISTER = 1 const val SOURCE_REGISTER = 1
const val SOURCE_CUSTOM = 2 const val SOURCE_CUSTOM = 2
const val SOURCE_SHARED = 3 const val SOURCE_SHARED = 3
fun getTypeColorMap() = mapOf(
TYPE_ELEARNING to COLOR_ELEARNING,
TYPE_HOMEWORK to COLOR_HOMEWORK,
TYPE_DEFAULT to COLOR_DEFAULT,
TYPE_EXAM to COLOR_EXAM,
TYPE_SHORT_QUIZ to COLOR_SHORT_QUIZ,
TYPE_ESSAY to COLOR_ESSAY,
TYPE_PROJECT to COLOR_PROJECT,
TYPE_PT_MEETING to COLOR_PT_MEETING,
TYPE_EXCURSION to COLOR_EXCURSION,
TYPE_READING to COLOR_READING,
TYPE_CLASS_EVENT to COLOR_CLASS_EVENT,
TYPE_INFORMATION to COLOR_INFORMATION
)
fun getTypeNameMap() = mapOf(
TYPE_ELEARNING to R.string.event_type_elearning,
TYPE_HOMEWORK to R.string.event_type_homework,
TYPE_DEFAULT to R.string.event_other,
TYPE_EXAM to R.string.event_exam,
TYPE_SHORT_QUIZ to R.string.event_short_quiz,
TYPE_ESSAY to R.string.event_essay,
TYPE_PROJECT to R.string.event_project,
TYPE_PT_MEETING to R.string.event_pt_meeting,
TYPE_EXCURSION to R.string.event_excursion,
TYPE_READING to R.string.event_reading,
TYPE_CLASS_EVENT to R.string.event_class_event,
TYPE_INFORMATION to R.string.event_information
)
} }
} }

View File

@ -50,6 +50,13 @@ open class Lesson(
var isExtra: Boolean = false var isExtra: Boolean = false
/**
* Stable ID denoting this lesson, used for note sharing (i.e. [profileId]-independent).
*
* This is simply the Unix timestamp of the lesson (in seconds).
*/
var ownerId: Long = id
val displayDate: Date? val displayDate: Date?
get() { get() {
if (type == TYPE_SHIFTED_SOURCE) if (type == TYPE_SHIFTED_SOURCE)
@ -69,7 +76,12 @@ open class Lesson(
val isChange val isChange
get() = type == TYPE_CHANGE || type == TYPE_SHIFTED_TARGET get() = type == TYPE_CHANGE || type == TYPE_SHIFTED_TARGET
fun buildId(): Long = (displayDate?.combineWith(displayStartTime) ?: 0L) / 6L * 10L + (hashCode() and 0xFFFF) fun buildId(): Long =
(displayDate?.combineWith(displayStartTime) ?: 0L) / 6L * 10L +
(hashCode() and 0xFFFF)
fun buildOwnerId(): Long =
(displayDate?.combineWith(displayStartTime) ?: 0L)
@Ignore @Ignore
var showAsUnseen = false var showAsUnseen = false

View File

@ -9,6 +9,7 @@ interface Noteable {
fun getNoteType(): Note.OwnerType fun getNoteType(): Note.OwnerType
fun getNoteOwnerProfileId(): Int fun getNoteOwnerProfileId(): Int
fun getNoteOwnerId(): Long fun getNoteOwnerId(): Long
fun getNoteShareTeamId(): Long? = null
var notes: MutableList<Note> var notes: MutableList<Note>

View File

@ -5,25 +5,18 @@
package pl.szczodrzynski.edziennik.data.db.entity package pl.szczodrzynski.edziennik.data.db.entity
import android.content.Context import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
import android.widget.ImageView import android.widget.ImageView
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore import androidx.room.Ignore
import com.google.gson.JsonObject import com.google.gson.JsonObject
import pl.droidsonroids.gif.GifDrawable
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.db.enums.LoginType import pl.szczodrzynski.edziennik.data.db.enums.LoginType
import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ext.dateToSemester
import pl.szczodrzynski.edziennik.utils.ProfileImageHolder import pl.szczodrzynski.edziennik.ext.getDrawable
import pl.szczodrzynski.edziennik.ext.getHolder
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.ImageHolder
import pl.szczodrzynski.navlib.R
import pl.szczodrzynski.navlib.drawer.IDrawerProfile import pl.szczodrzynski.navlib.drawer.IDrawerProfile
import pl.szczodrzynski.navlib.getDrawableFromRes
@Entity(tableName = "profiles", primaryKeys = ["profileId"]) @Entity(tableName = "profiles", primaryKeys = ["profileId"])
open class Profile( open class Profile(
@ -60,9 +53,13 @@ open class Profile(
} }
override var image: String? = null override var image: String? = null
var empty = true var empty = true
var archived = false var archived = false
var syncEnabled = true
@ColumnInfo(name = "enableSharedEvents")
var unused1 = true
var registration = REGISTRATION_UNSPECIFIED
var userCode = ""
/** /**
* A unique ID matching [archived] profiles with current ones * A unique ID matching [archived] profiles with current ones
@ -70,117 +67,35 @@ open class Profile(
*/ */
var archiveId: Int? = null var archiveId: Int? = null
var syncEnabled = true
var enableSharedEvents = true
var registration = REGISTRATION_UNSPECIFIED
var userCode = ""
/** /**
* The student's number in the class register. * The student's number in the class register.
*/ */
var studentNumber = -1 var studentNumber = -1
var studentClassName: String? = null var studentClassName: String? = null
var studentSchoolYearStart = Date.getToday().let { if (it.month < 9) it.year - 1 else it.year } var studentSchoolYearStart = Date.getToday().let { if (it.month < 9) it.year - 1 else it.year }
var dateSemester1Start = Date(studentSchoolYearStart, 9, 1) var dateSemester1Start = Date(studentSchoolYearStart, 9, 1)
var dateSemester2Start = Date(studentSchoolYearStart + 1, 2, 1) var dateSemester2Start = Date(studentSchoolYearStart + 1, 2, 1)
var dateYearEnd = Date(studentSchoolYearStart + 1, 6, 30) var dateYearEnd = Date(studentSchoolYearStart + 1, 6, 30)
fun getSemesterStart(semester: Int) = if (semester == 1) dateSemester1Start else dateSemester2Start
fun getSemesterEnd(semester: Int) = if (semester == 1) dateSemester2Start.clone().stepForward(0, 0, -1) else dateYearEnd
fun dateToSemester(date: Date) = if (date >= dateSemester2Start) 2 else 1
@delegate:Ignore
val currentSemester by lazy { dateToSemester(Date.getToday()) }
fun shouldArchive(): Boolean {
// vulcan hotfix
if (dateYearEnd.month > 6) {
dateYearEnd.month = 6
dateYearEnd.day = 30
}
// fix for when versions <4.3 synced 2020/2021 year dates to older profiles during 2020 Jun-Aug
if (dateSemester1Start.year > studentSchoolYearStart) {
val diff = dateSemester1Start.year - studentSchoolYearStart
dateSemester1Start.year -= diff
dateSemester2Start.year -= diff
dateYearEnd.year -= diff
}
return App.config.archiverEnabled
&& Date.getToday() >= dateYearEnd
&& Date.getToday().year > studentSchoolYearStart
}
fun isBeforeYear() = false && Date.getToday() < dateSemester1Start
var disabledNotifications: List<Long>? = null var disabledNotifications: List<Long>? = null
var lastReceiversSync: Long = 0 var lastReceiversSync: Long = 0
fun hasStudentData(key: String) = studentData.has(key) val currentSemester
fun getStudentData(key: String, defaultValue: Boolean) = studentData.getBoolean(key) ?: defaultValue get() = dateToSemester(Date.getToday())
fun getStudentData(key: String, defaultValue: String?) = studentData.getString(key) ?: defaultValue
fun getStudentData(key: String, defaultValue: Int) = studentData.getInt(key) ?: defaultValue
fun getStudentData(key: String, defaultValue: Long) = studentData.getLong(key) ?: defaultValue
fun getStudentData(key: String, defaultValue: Float) = studentData.getFloat(key) ?: defaultValue
fun getStudentData(key: String, defaultValue: Char) = studentData.getChar(key) ?: defaultValue
fun putStudentData(key: String, value: Boolean) { studentData[key] = value }
fun putStudentData(key: String, value: String?) { studentData[key] = value }
fun putStudentData(key: String, value: Number) { studentData[key] = value }
fun putStudentData(key: String, value: Char) { studentData[key] = value }
fun removeStudentData(key: String) { studentData.remove(key) }
val isParent val isParent
get() = accountName != null get() = accountName != null
val accountOwnerName val accountOwnerName
get() = accountName ?: studentNameLong get() = accountName ?: studentNameLong
val registerName
@Ignore get() = loginStoreType.name.lowercase()
val registerName = loginStoreType.name.lowercase()
val canShare val canShare
get() = registration == REGISTRATION_ENABLED && !archived get() = registration == REGISTRATION_ENABLED && !archived
override fun getImageDrawable(context: Context): Drawable { @delegate:Ignore
if (archived) { @delegate:Transient
return context.getDrawableFromRes(pl.szczodrzynski.edziennik.R.drawable.profile_archived).also { val config by lazy { App.config[this.id] }
it.colorFilter = PorterDuffColorFilter(colorFromName(name), PorterDuff.Mode.DST_OVER)
}
}
if (!image.isNullOrEmpty()) { override fun getImageDrawable(context: Context) = this.getDrawable(context)
try { override fun getImageHolder(context: Context) = this.getHolder()
return if (image?.endsWith(".gif", true) == true) {
GifDrawable(image ?: "")
} else {
RoundedBitmapDrawableFactory.create(context.resources, image ?: "")
//return Drawable.createFromPath(image ?: "") ?: throw Exception()
}
}
catch (e: Exception) {
e.printStackTrace()
}
}
return context.getDrawableFromRes(R.drawable.profile).also {
it.colorFilter = PorterDuffColorFilter(colorFromName(name), PorterDuff.Mode.DST_OVER)
}
}
override fun getImageHolder(context: Context): ImageHolder {
if (archived) {
return ImageHolder(pl.szczodrzynski.edziennik.R.drawable.profile_archived, colorFromName(name))
}
return if (!image.isNullOrEmpty()) {
try {
ProfileImageHolder(image ?: "")
} catch (_: Exception) {
ImageHolder(R.drawable.profile, colorFromName(name))
}
}
else {
ImageHolder(R.drawable.profile, colorFromName(name))
}
}
override fun applyImageTo(imageView: ImageView) { override fun applyImageTo(imageView: ImageView) {
getImageHolder(imageView.context).applyTo(imageView) getImageHolder(imageView.context).applyTo(imageView)
} }

View File

@ -6,6 +6,8 @@ package pl.szczodrzynski.edziennik.data.db.enums
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.ext.getString
import pl.szczodrzynski.edziennik.ext.isNotNullNorBlank
enum class LoginMethod( enum class LoginMethod(
val loginType: LoginType, val loginType: LoginType,
@ -26,7 +28,7 @@ enum class LoginMethod(
MOBIDZIENNIK_API2( MOBIDZIENNIK_API2(
loginType = LoginType.MOBIDZIENNIK, loginType = LoginType.MOBIDZIENNIK,
id = 1300, id = 1300,
isPossible = { profile, _ -> profile?.studentData?.has("email") ?: false }, isPossible = { profile, _ -> profile?.studentData?.getString("email").isNotNullNorBlank() },
), ),
LIBRUS_PORTAL( LIBRUS_PORTAL(
loginType = LoginType.LIBRUS, loginType = LoginType.LIBRUS,
@ -57,7 +59,7 @@ enum class LoginMethod(
VULCAN_WEB_MAIN( VULCAN_WEB_MAIN(
loginType = LoginType.VULCAN, loginType = LoginType.VULCAN,
id = 4100, id = 4100,
isPossible = { _, loginStore -> loginStore.hasLoginData("webHost") }, isPossible = { _, loginStore -> loginStore.getLoginData("webHost", null).isNotNullNorBlank() },
), ),
VULCAN_HEBE( VULCAN_HEBE(
loginType = LoginType.VULCAN, loginType = LoginType.VULCAN,

View File

@ -17,4 +17,5 @@ enum class LoginMode(
VULCAN_HEBE(LoginType.VULCAN, id = 402), VULCAN_HEBE(LoginType.VULCAN, id = 402),
PODLASIE_API(LoginType.PODLASIE, id = 600), PODLASIE_API(LoginType.PODLASIE, id = 600),
USOS_OAUTH(LoginType.USOS, id = 700), USOS_OAUTH(LoginType.USOS, id = 700),
DEMO(LoginType.DEMO, id = 800),
} }

View File

@ -7,13 +7,14 @@ package pl.szczodrzynski.edziennik.data.db.enums
enum class LoginType( enum class LoginType(
val id: Int, val id: Int,
val features: Set<FeatureType>, val features: Set<FeatureType>,
val schoolType: SchoolType = SchoolType.STANDARD,
) { ) {
MOBIDZIENNIK(id = 1, features = FEATURES_MOBIDZIENNIK), MOBIDZIENNIK(id = 1, features = FEATURES_MOBIDZIENNIK),
LIBRUS(id = 2, features = FEATURES_LIBRUS), LIBRUS(id = 2, features = FEATURES_LIBRUS),
VULCAN(id = 4, features = FEATURES_VULCAN), VULCAN(id = 4, features = FEATURES_VULCAN),
PODLASIE(id = 6, features = FEATURES_PODLASIE), PODLASIE(id = 6, features = FEATURES_PODLASIE),
USOS(id = 7, features = FEATURES_USOS), USOS(id = 7, features = FEATURES_USOS, schoolType = SchoolType.UNIVERSITY),
DEMO(id = 20, features = setOf()), DEMO(id = 8, features = setOf()),
TEMPLATE(id = 21, features = setOf()), TEMPLATE(id = 21, features = setOf()),
// the graveyard // the graveyard

View File

@ -0,0 +1,10 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-21.
*/
package pl.szczodrzynski.edziennik.data.db.enums
enum class SchoolType {
STANDARD,
UNIVERSITY,
}

View File

@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Note import pl.szczodrzynski.edziennik.data.db.entity.Note
import pl.szczodrzynski.edziennik.data.db.entity.Noteable import pl.szczodrzynski.edziennik.data.db.entity.Noteable
import pl.szczodrzynski.edziennik.ext.takePositive
import pl.szczodrzynski.edziennik.ui.search.Searchable import pl.szczodrzynski.edziennik.ui.search.Searchable
import pl.szczodrzynski.edziennik.utils.html.BetterHtml import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
@ -118,4 +119,5 @@ class EventFull(
override fun getNoteType() = Note.OwnerType.EVENT override fun getNoteType() = Note.OwnerType.EVENT
override fun getNoteOwnerProfileId() = profileId override fun getNoteOwnerProfileId() = profileId
override fun getNoteOwnerId() = id override fun getNoteOwnerId() = id
override fun getNoteShareTeamId() = teamId.takePositive()
} }

View File

@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.entity.Lesson
import pl.szczodrzynski.edziennik.data.db.entity.Note import pl.szczodrzynski.edziennik.data.db.entity.Note
import pl.szczodrzynski.edziennik.data.db.entity.Noteable import pl.szczodrzynski.edziennik.data.db.entity.Noteable
import pl.szczodrzynski.edziennik.ext.takePositive
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
class LessonFull( class LessonFull(
@ -137,9 +138,10 @@ class LessonFull(
var seen: Boolean = false var seen: Boolean = false
var notified: Boolean = false var notified: Boolean = false
@Relation(parentColumn = "id", entityColumn = "noteOwnerId", entity = Note::class) @Relation(parentColumn = "ownerId", entityColumn = "noteOwnerId", entity = Note::class)
override lateinit var notes: MutableList<Note> override lateinit var notes: MutableList<Note>
override fun getNoteType() = Note.OwnerType.LESSON override fun getNoteType() = Note.OwnerType.LESSON
override fun getNoteOwnerProfileId() = profileId override fun getNoteOwnerProfileId() = profileId
override fun getNoteOwnerId() = id override fun getNoteOwnerId() = ownerId
override fun getNoteShareTeamId() = teamId.takePositive()
} }

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-25.
*/
package pl.szczodrzynski.edziennik.data.db.migration
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration100 : Migration(99, 100) {
override fun migrate(database: SupportSQLiteDatabase) {
// add Note Owner ID to Lesson, to make it profileId-independent
// calculate the new owner ID based on the old ID
database.execSQL("ALTER TABLE timetable ADD COLUMN ownerId INT NOT NULL DEFAULT 0;")
// set new ID for actual lessons
database.execSQL("UPDATE timetable SET ownerId = ROUND((id & ~65535) / 500000.0) * 300000;")
// copy the old ID (date value) for NO_LESSONS
database.execSQL("UPDATE timetable SET ownerId = id WHERE type = -1;")
// update ID for notes as well
database.execSQL("UPDATE notes SET noteOwnerId = ROUND((noteOwnerId & ~65535) / 500000.0) * 300000 WHERE noteOwnerType = 'LESSON' AND noteOwnerId > 2000000000000;")
// force full app sync to download notes with new IDs
database.execSQL("DELETE FROM config WHERE `key` = 'hash';")
database.execSQL("DELETE FROM config WHERE `key` = 'lastAppSync';")
}
}

View File

@ -6,7 +6,11 @@ package pl.szczodrzynski.edziennik.data.firebase
import com.google.gson.JsonParser import com.google.gson.JsonParser
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.events.FeedbackMessageEvent import pl.szczodrzynski.edziennik.data.api.events.FeedbackMessageEvent
@ -14,14 +18,18 @@ import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent
import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.api.task.PostNotifications import pl.szczodrzynski.edziennik.data.api.task.PostNotifications
import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Note
import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
import pl.szczodrzynski.edziennik.data.db.enums.NotificationType import pl.szczodrzynski.edziennik.data.db.enums.NotificationType
import pl.szczodrzynski.edziennik.ext.getInt import pl.szczodrzynski.edziennik.ext.getInt
import pl.szczodrzynski.edziennik.ext.getLong import pl.szczodrzynski.edziennik.ext.getLong
import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.ext.getString
import pl.szczodrzynski.edziennik.ext.resolveString import pl.szczodrzynski.edziennik.ext.resolveString
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
@ -64,7 +72,10 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
message.data.getString("title") ?: "", message.data.getString("title") ?: "",
message.data.getString("message") ?: "" message.data.getString("message") ?: ""
) )
"appUpdate" -> launch { UpdateWorker.runNow(app, app.gson.fromJson(message.data.getString("update"), Update::class.java)) } "appUpdate" -> {
val update = app.gson.fromJson(message.data.getString("update"), Update::class.java)
app.updateManager.process(update, notify = true)
}
"feedbackMessage" -> launch { "feedbackMessage" -> launch {
val message = app.gson.fromJson(message.data.getString("message"), FeedbackMessage::class.java) ?: return@launch val message = app.gson.fromJson(message.data.getString("message"), FeedbackMessage::class.java) ?: return@launch
feedbackMessage(message) feedbackMessage(message)
@ -149,11 +160,11 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
if (event.color == -1) if (event.color == -1)
event.color = null event.color = null
event.addedManually = true
event.sharedBy = json.getString("sharedBy") event.sharedBy = json.getString("sharedBy")
event.sharedByName = json.getString("sharedByName") event.sharedByName = json.getString("sharedByName")
if (profile.userCode == event.sharedBy) { if (profile.userCode == event.sharedBy) {
event.sharedBy = "self" event.sharedBy = "self"
event.addedManually = true
} }
val metadata = Metadata( val metadata = Metadata(
@ -165,7 +176,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
) )
val type = if (event.isHomework) NotificationType.SHARED_HOMEWORK else NotificationType.SHARED_EVENT val type = if (event.isHomework) NotificationType.SHARED_HOMEWORK else NotificationType.SHARED_EVENT
val notificationFilter = app.config.getFor(event.profileId).sync.notificationFilter val notificationFilter = app.config[event.profileId].sync.notificationFilter
if (!notificationFilter.contains(type) && event.sharedBy != "self" && event.date >= Date.getToday()) { if (!notificationFilter.contains(type) && event.sharedBy != "self" && event.date >= Date.getToday()) {
val notification = Notification( val notification = Notification(
@ -200,7 +211,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@forEach val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@forEach
if (!profile.canShare) if (!profile.canShare)
return@forEach return@forEach
val notificationFilter = app.config.getFor(team.profileId).sync.notificationFilter val notificationFilter = app.config[team.profileId].sync.notificationFilter
if (!notificationFilter.contains(NotificationType.REMOVED_SHARED_EVENT)) { if (!notificationFilter.contains(NotificationType.REMOVED_SHARED_EVENT)) {
val notification = Notification( val notification = Notification(
@ -254,7 +265,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
return@forEach return@forEach
val type = NotificationType.SHARED_NOTE val type = NotificationType.SHARED_NOTE
val notificationFilter = app.config.getFor(note.profileId).sync.notificationFilter val notificationFilter = app.config[note.profileId].sync.notificationFilter
if (!notificationFilter.contains(type) && note.sharedBy != "self") { if (!notificationFilter.contains(type) && note.sharedBy != "self") {
val notification = Notification( val notification = Notification(

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