Compare commits

..

No commits in common. "master" and "v4.13-rc.3" have entirely different histories.

184 changed files with 1439 additions and 5332 deletions

View file

@ -23,6 +23,8 @@ 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,9 +102,7 @@ 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,8 +1,11 @@
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,
@ -22,6 +25,17 @@ 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)
@ -34,8 +48,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("appVersionName=" + version_name) print("::set-output name=appVersionName::" + version_name)
print("appVersionCode=" + str(version_code)) print("::set-output name=appVersionCode::" + str(version_code))
write_gradle_version(project_dir, version_code, version_name) write_gradle_version(project_dir, version_code, version_name)

View file

@ -1,23 +0,0 @@
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("appVersionName=" + version_name) print("::set-output name=appVersionName::" + version_name)
print("appVersionCode=" + str(version_code)) print("::set-output name=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("changelogDir=" + dir) print("::set-output name=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("changelogPlainTitledFile=" + dir + "whatsnew_titled.txt") print("::set-output name=changelogPlainTitledFile::" + dir + "whatsnew-titled.txt")
print("changelogTitle=" + title) print("::set-output name=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,31 +41,32 @@ if __name__ == "__main__":
changelog = changelog.strip() changelog = changelog.strip()
f.write(changelog) f.write(changelog)
print("changelogPlainFile=" + dir + "whatsnew-pl-PL") print("::set-output name=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("changelogMarkdownFile=" + dir + "whatsnew.md") print("::set-output name=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("changelogHtmlFile=" + dir + "whatsnew.html") print("::set-output name=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("commitLogPlainFile=" + dir + "commit_log.txt") print("::set-output name=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("commitLogMarkdownFile=" + dir + "commit_log.md") print("::set-output name=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("commitLogHtmlFile=" + dir + "commit_log.html") print("::set-output name=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(project_dir + "/", "") file_relative = file.replace(os.getenv("GITHUB_WORKSPACE") + "/", "")
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("signedReleaseFile=" + file) print("::set-output name=signedReleaseFile::" + file)
print("signedReleaseFileRelative=" + file_relative) print("::set-output name=signedReleaseFileRelative::" + file_relative)

View file

@ -64,14 +64,7 @@ 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",
@ -126,12 +119,4 @@ 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( save_version(project_dir, DB_HOST, DB_USER, DB_PASS, DB_NAME, APK_SERVER_RELEASE, APK_SERVER_NIGHTLY)
project_dir,
DB_HOST,
DB_USER,
DB_PASS,
DB_NAME,
APK_SERVER_RELEASE,
APK_SERVER_NIGHTLY,
)

View file

@ -31,6 +31,8 @@ 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])
@ -69,8 +71,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("appVersionName=" + version_name) print("::set-output name=appVersionName::" + version_name)
print("appVersionCode=" + str(version_code)) print("::set-output name=appVersionCode::" + str(version_code))
sign( sign(
project_dir, project_dir,

View file

@ -11,7 +11,8 @@ 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,
download_url: str, apk_server_release: str,
apk_server_nightly: str,
webhook_release: str, webhook_release: str,
webhook_testing: str, webhook_testing: str,
): ):
@ -24,6 +25,12 @@ 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:
@ -41,17 +48,13 @@ 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(version_name, changelog, download_url) webhook = get_webhook_release(changelog, download_url)
requests.post(url=webhook_release, json=webhook) requests.post(url=webhook_release, json=webhook)
def get_webhook_release(version_name: str, changelog: str, download_url: str): def get_webhook_release(changelog: str, download_url: str):
(title, content) = changelog (title, content) = changelog
return { return {"content": f"__**{title}**__\n{content}\n{download_url}"}
"content": (
f"__**{title}**__\n{content}\n[Szkolny.eu {version_name}]({download_url})"
),
}
def get_webhook_testing( def get_webhook_testing(
@ -70,11 +73,9 @@ def get_webhook_testing(
"fields": [ "fields": [
{ {
"name": f"Wersja `{version_name}`", "name": f"Wersja `{version_name}`",
"value": ( "value": f"[Pobierz .APK]({download_url})"
f"[Pobierz .APK]({download_url})"
if download_url if download_url
else "*Pobieranie niedostępne*" else "*Pobieranie niedostępne*",
),
"inline": False, "inline": False,
}, },
{ {
@ -102,14 +103,16 @@ if __name__ == "__main__":
load_dotenv() load_dotenv()
APK_FILE = os.getenv("APK_FILE") APK_FILE = os.getenv("APK_FILE")
DOWNLOAD_URL = os.getenv("DOWNLOAD_URL") APK_SERVER_RELEASE = os.getenv("APK_SERVER_RELEASE")
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,
DOWNLOAD_URL, APK_SERVER_RELEASE,
APK_SERVER_NIGHTLY,
WEBHOOK_RELEASE, WEBHOOK_RELEASE,
WEBHOOK_TESTING, WEBHOOK_TESTING,
) )

View file

@ -1,195 +0,0 @@
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@v4
if: always()
with:
name: ${{ steps.changelog.outputs.appVersionName }}
path: |
app/release/whatsnew*/
app/release/*.apk
app/release/*.aab
app/release/*.json
app/release/*.txt

154
.github/workflows/build-nightly-apk.yml vendored Normal file
View file

@ -0,0 +1,154 @@
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

@ -0,0 +1,131 @@
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

154
.github/workflows/build-release-apk.yml vendored Normal file
View file

@ -0,0 +1,154 @@
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

View file

@ -1,13 +0,0 @@
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

View file

@ -1,15 +0,0 @@
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

View file

@ -1,42 +0,0 @@
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

@ -104,6 +104,7 @@ android {
externalNativeBuild { externalNativeBuild {
cmake { cmake {
path "src/main/cpp/CMakeLists.txt" path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
} }
} }
lint { lint {
@ -112,9 +113,8 @@ android {
} }
tasks.whenTaskAdded { task -> tasks.whenTaskAdded { task ->
if (!(task.name == "assembleUnofficialRelease" || task.name == "assembleOfficialRelease" || task.name == "signPlayReleaseBundle")) if (!task.name.endsWith("Release") && !task.name.endsWith("ReleaseWithR8"))
return return
def renameTaskName = "rename${task.name.capitalize()}" def renameTaskName = "rename${task.name.capitalize()}"
def flavor = "" def flavor = ""
@ -124,22 +124,17 @@ 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.register(renameTaskName, Copy) { tasks.create(renameTaskName, Copy) {
dependsOn(task.name)
duplicatesStrategy DuplicatesStrategy.FAIL
from file("${projectDir}/${flavor}/release/"), from file("${projectDir}/${flavor}/release/"),
file("${projectDir}/build/outputs/apk/${flavor}/release/"), file("${buildDir}/outputs/mapping/${flavor}Release/"),
file("${projectDir}/build/outputs/mapping/${flavor}Release/"), file("${buildDir}/outputs/apk/${flavor}/release/"),
file("${projectDir}/build/outputs/bundle/${flavor}Release/") file("${buildDir}/outputs/bundle/${flavor}Release/")
include "*-release.aab", "*-release.apk", "mapping.txt", "output-metadata.json" include "*.aab", "*.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)
} }
} }
@ -161,7 +156,6 @@ 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"

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,6 @@
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- PowerPermission uses minSdk 21, it's safe to override as it is used only in >= 23 --> <!-- PowerPermission uses minSdk 21, it's safe to override as it is used only in >= 23 -->
<uses-sdk tools:overrideLibrary="com.qifan.powerpermission.coroutines, com.qifan.powerpermission.core" /> <uses-sdk tools:overrideLibrary="com.qifan.powerpermission.coroutines, com.qifan.powerpermission.core" />
@ -85,7 +84,7 @@
android:resource="@xml/widget_timetable_info" /> android:resource="@xml/widget_timetable_info" />
</receiver> </receiver>
<service android:name=".ui.widgets.timetable.WidgetTimetableService" <service android:name=".ui.widgets.timetable.WidgetTimetableService"
android:permission="android.permission.BIND_REMOTEVIEWS" android:foregroundServiceType="dataSync" /> android:permission="android.permission.BIND_REMOTEVIEWS" />
<activity android:name=".ui.widgets.LessonDialogActivity" <activity android:name=".ui.widgets.LessonDialogActivity"
android:label="" android:label=""
android:configChanges="orientation|keyboardHidden" android:configChanges="orientation|keyboardHidden"
@ -106,7 +105,7 @@
android:resource="@xml/widget_notifications_info" /> android:resource="@xml/widget_notifications_info" />
</receiver> </receiver>
<service android:name=".ui.widgets.notifications.WidgetNotificationsService" <service android:name=".ui.widgets.notifications.WidgetNotificationsService"
android:permission="android.permission.BIND_REMOTEVIEWS" android:foregroundServiceType="dataSync" /> android:permission="android.permission.BIND_REMOTEVIEWS" />
<!-- LUCKY NUMBER --> <!-- LUCKY NUMBER -->
<receiver android:name=".ui.widgets.luckynumber.WidgetLuckyNumberProvider" <receiver android:name=".ui.widgets.luckynumber.WidgetLuckyNumberProvider"
android:label="@string/widget_lucky_number_title" android:label="@string/widget_lucky_number_title"
@ -161,11 +160,7 @@
<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/Theme.MaterialComponents.Light.DarkActionBar" /> android:theme="@style/AppTheme.Light" />
<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" />
@ -203,15 +198,15 @@
____) | __/ | \ V /| | (_| __/\__ \ ____) | __/ | \ V /| | (_| __/\__ \
|_____/ \___|_| \_/ |_|\___\___||___/ |_____/ \___|_| \_/ |_|\___\___||___/
--> -->
<service android:name=".data.api.ApiService" android:foregroundServiceType="dataSync" /> <service android:name=".data.api.ApiService" />
<service android:name=".data.firebase.MyFirebaseService" <service android:name=".data.firebase.MyFirebaseService"
android:exported="false" android:foregroundServiceType="dataSync"> android:exported="false">
<intent-filter android:priority="10000000"> <intent-filter android:priority="10000000">
<action android:name="com.google.firebase.MESSAGING_EVENT" /> <action android:name="com.google.firebase.MESSAGING_EVENT" />
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" /> <action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter> </intent-filter>
</service> </service>
<service android:name=".sync.UpdateDownloaderService" android:foregroundServiceType="dataSync" /> <service android:name=".sync.UpdateDownloaderService" />
<!-- <!--
_____ _ _ _____ _ _

View file

@ -1,13 +1,13 @@
<h3>Wersja 4.14.1, 2025-02-05</h3> <h3>Wersja 4.13-rc.3, 2022-10-23</h3>
<ul> <ul>
<li>USOS: <b>dodano obsługę ocen</b>.</li> <li>Poprawione powiadomienia na Androidzie 13. @santoni0</li>
<li>USOS: obliczanie średniej za studia oraz punktów ECTS.</li> <li>Możliwość dostosowania wyświetlania planu lekcji.</li>
<li>USOS: poprawiono brak planu zajęć po rozpoczęciu roku.</li> <li>Opcja kolorowania bloków w planie lekcji.</li>
<li>Wyłączono archiwizator profili.</li> <li><b>USOS</b> - pierwsza wersja obsługi systemu. Osobne rodzaje wydarzeń (oraz wygląd niektórych części aplikacji) lepiej dostosowany do nauki na studiach.</li>
<li>Naprawiono zatrzymanie aplikacji na Androidzie 14.</li> <li>Poprawione opcje filtrowania powiadomień i wyboru przycisków menu bocznego.</li>
<li>Udostępniono opcję wyłączania śniegu również w lutym.</li> <li>Ulepszony system pobierania aktualizacji aplikacji.</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) 2025</i> <i>&copy; [Kuba Szczodrzyński](@kuba2k2), [Kacper Ziubryniewicz](@kapi2289) 2022</i>

View file

@ -5,8 +5,6 @@
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.

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] = {
0x58, 0x51, 0x7b, 0xb1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; 0x0f, 0x0d, 0x19, 0x4d, 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

@ -235,20 +235,11 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
} }
Signing.getCert(this) Signing.getCert(this)
Utils.initializeStorageDir(this)
launch { launch {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
config.migrate(this@App) config.migrate(this@App)
if (config.appVersionCore < BuildConfig.VERSION_CODE) {
// force syncing all endpoints on update
db.endpointTimerDao().clear()
config.sync.lastAppSync = 0L
config.hash = "invalid"
config.appVersionCore = BuildConfig.VERSION_CODE
}
SSLProviderInstaller.install(applicationContext, this@App::buildHttp) SSLProviderInstaller.install(applicationContext, this@App::buildHttp)
if (config.devModePassword != null) if (config.devModePassword != null)
@ -431,12 +422,6 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
try { try {
App.data = AppData.get(profile.loginStoreType) App.data = AppData.get(profile.loginStoreType)
d("App", "Loaded AppData: ${App.data}") 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) { } catch (e: Exception) {
Log.e("App", "Cannot load AppData", e) Log.e("App", "Cannot load AppData", e)
Toast.makeText(this, R.string.app_cannot_load_data, Toast.LENGTH_LONG).show() Toast.makeText(this, R.string.app_cannot_load_data, Toast.LENGTH_LONG).show()
@ -477,8 +462,6 @@ 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

@ -15,8 +15,6 @@ import android.view.Gravity
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
@ -324,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 / 3 % 4 == 0) && app.config.ui.snowfall) { if ((today.month % 11 == 1) && 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(
@ -831,12 +829,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
d(TAG, "Activity resumed") d(TAG, "Activity resumed")
val filter = IntentFilter() val filter = IntentFilter()
filter.addAction(Intent.ACTION_MAIN) filter.addAction(Intent.ACTION_MAIN)
ActivityCompat.registerReceiver( registerReceiver(intentReceiver, filter)
this,
intentReceiver,
filter,
ContextCompat.RECEIVER_EXPORTED,
)
EventBus.getDefault().register(this) EventBus.getDefault().register(this)
super.onResume() super.onResume()
} }

View file

@ -59,11 +59,10 @@ data class AppData(
val lessonHeight: Int, val lessonHeight: Int,
val enableMarkAsReadAnnouncements: Boolean, val enableMarkAsReadAnnouncements: Boolean,
val enableNoticePoints: Boolean, val enableNoticePoints: Boolean,
val eventManualShowSubjectDropdown: Boolean,
) )
data class EventType( data class EventType(
val id: Long, val id: Int,
val color: String, val color: String,
val name: String, val name: String,
) )

View file

@ -14,7 +14,6 @@ import pl.szczodrzynski.edziennik.ext.takePositive
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
abstract class BaseConfig( abstract class BaseConfig(
@Transient
val db: AppDb, val db: AppDb,
val profileId: Int? = null, val profileId: Int? = null,
protected var entries: List<ConfigEntry>? = null, protected var entries: List<ConfigEntry>? = null,
@ -43,6 +42,4 @@ abstract class BaseConfig(
db.configDao().add(ConfigEntry(profileId ?: -1, key, value)) db.configDao().add(ConfigEntry(profileId ?: -1, key, value))
} }
} }
fun has(key: String) = values.containsKey(key)
} }

View file

@ -33,7 +33,7 @@ class Config(db: AppDb) : BaseConfig(db) {
var update by config<Update?>(null) var update by config<Update?>(null)
var updatesChannel by config<String>("release") var updatesChannel by config<String>("release")
var devMode by config<Boolean?>("debugMode", null) var devMode by config<Boolean?>(null)
var devModePassword by config<String?>(null) var devModePassword by config<String?>(null)
var enableChucker by config<Boolean?>(null) var enableChucker by config<Boolean?>(null)
@ -43,7 +43,6 @@ class Config(db: AppDb) : BaseConfig(db) {
var appInstalledTime by config<Long>(0L) var appInstalledTime by config<Long>(0L)
var appRateSnackbarTime by config<Long>(0L) var appRateSnackbarTime by config<Long>(0L)
var appVersion by config<Int>(BuildConfig.VERSION_CODE) var appVersion by config<Int>(BuildConfig.VERSION_CODE)
var appVersionCore by config<Int>(0)
var validation by config<String?>(null, "buildValidation") var validation by config<String?>(null, "buildValidation")
var archiverEnabled by config<Boolean>(true) var archiverEnabled by config<Boolean>(true)
@ -56,9 +55,11 @@ class Config(db: AppDb) : BaseConfig(db) {
ConfigMigration(app, this) ConfigMigration(app, this)
} }
operator fun get(profileId: Int): ProfileConfig { fun getFor(profileId: Int): ProfileConfig {
return profileConfigs[profileId] ?: ProfileConfig(db, profileId, entries).also { return profileConfigs[profileId] ?: ProfileConfig(db, profileId, entries).also {
profileConfigs[profileId] = it profileConfigs[profileId] = it
} }
} }
fun forProfile() = getFor(App.profileId)
} }

View file

@ -115,9 +115,9 @@ class ConfigDelegate<T>(
is Boolean -> value is Boolean -> value
// enums, maps & collections // enums, maps & collections
is Enum<*> -> value.toInt() is Enum<*> -> value.toInt()
is Collection<*> -> value.map { is Collection<*> -> JsonArray(value.map {
if (it is Number || it is Boolean) it else serialize(it, serializeObjects = false) if (it is Number || it is Boolean) it else serialize(it, serializeObjects = false)
}.toJsonElement() })
is Map<*, *> -> gson.toJson(value.mapValues { (_, it) -> is Map<*, *> -> gson.toJson(value.mapValues { (_, it) ->
if (it is Number || it is Boolean) it else serialize(it, serializeObjects = false) if (it is Number || it is Boolean) it else serialize(it, serializeObjects = false)
}) })

View file

@ -15,7 +15,7 @@ class ProfileConfig(
entries: List<ConfigEntry>?, entries: List<ConfigEntry>?,
) : BaseConfig(db, profileId, entries) { ) : BaseConfig(db, profileId, entries) {
companion object { companion object {
const val DATA_VERSION = 5 const val DATA_VERSION = 4
} }
val grades by lazy { ProfileConfigGrades(this) } val grades by lazy { ProfileConfigGrades(this) }
@ -29,8 +29,6 @@ class ProfileConfig(
var dataVersion by config<Int>(DATA_VERSION) var dataVersion by config<Int>(DATA_VERSION)
var hash by config<String>("") var hash by config<String>("")
var shareByDefault by config<Boolean>(false)
init { init {
if (dataVersion < DATA_VERSION) if (dataVersion < DATA_VERSION)
ProfileConfigMigration(this) ProfileConfigMigration(this)

View file

@ -4,7 +4,6 @@
package pl.szczodrzynski.edziennik.config package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.UNIVERSITY_AVERAGE_MODE_ECTS
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
@ -16,11 +15,8 @@ class ProfileConfigGrades(base: ProfileConfig) {
var dontCountEnabled by base.config<Boolean>(false) var dontCountEnabled by base.config<Boolean>(false)
var dontCountGrades by base.config<List<String>> { listOf() } var dontCountGrades by base.config<List<String>> { listOf() }
var hideImproved by base.config<Boolean>(false) var hideImproved by base.config<Boolean>(false)
var hideNoGrade by base.config<Boolean>(false)
var hideSticksFromOld by base.config<Boolean>(false) var hideSticksFromOld by base.config<Boolean>(false)
var minusValue by base.config<Float?>(null) var minusValue by base.config<Float?>(null)
var plusValue by base.config<Float?>(null) var plusValue by base.config<Float?>(null)
var yearAverageMode by base.config<Int>(YEAR_ALL_GRADES) var yearAverageMode by base.config<Int>(YEAR_ALL_GRADES)
var universityAverageMode by base.config<Int>(UNIVERSITY_AVERAGE_MODE_ECTS)
var countEctsInProgress by base.config<Boolean>(false)
} }

View file

@ -15,7 +15,6 @@ class ProfileConfigUI(base: ProfileConfig) {
var agendaGroupByType by base.config<Boolean>(false) var agendaGroupByType by base.config<Boolean>(false)
var agendaLessonChanges by base.config<Boolean>(true) var agendaLessonChanges by base.config<Boolean>(true)
var agendaTeacherAbsence 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 agendaElearningMark by base.config<Boolean>(false)
var agendaElearningGroup by base.config<Boolean>(true) var agendaElearningGroup by base.config<Boolean>(true)

View file

@ -16,8 +16,6 @@ import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_AL
class ProfileConfigMigration(config: ProfileConfig) { class ProfileConfigMigration(config: ProfileConfig) {
init { config.apply { init { config.apply {
val profile = db.profileDao().getByIdNow(profileId ?: -1)
if (dataVersion < 2) { if (dataVersion < 2) {
sync.notificationFilter = sync.notificationFilter + NotificationType.TEACHER_ABSENCE sync.notificationFilter = sync.notificationFilter + NotificationType.TEACHER_ABSENCE
@ -39,23 +37,11 @@ class ProfileConfigMigration(config: ProfileConfig) {
// switch to new event types (USOS) // switch to new event types (USOS)
dataVersion = 4 dataVersion = 4
val profile = db.profileDao().getByIdNow(profileId ?: -1)
if (profile?.loginStoreType?.schoolType == SchoolType.UNIVERSITY) { if (profile?.loginStoreType?.schoolType == SchoolType.UNIVERSITY) {
db.eventTypeDao().clear(profileId ?: -1) db.eventTypeDao().clear(profileId ?: -1)
db.eventTypeDao().addDefaultTypes(profile) 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,10 +26,9 @@ 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/konto-librus/redirect/dru" 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_LOGIN_URL = "https://portal.librus.pl/konto-librus/login/action" const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/rodzina/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"
@ -60,6 +59,9 @@ 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,25 +24,6 @@ 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,7 +9,6 @@ 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
@ -29,8 +28,6 @@ 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
@ -114,11 +111,6 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
} }
} }
if (profile?.empty == true) {
// force app sync on first login
app.config.sync.lastAppSync = 0L
}
edziennikInterface = when (loginStore.type) { edziennikInterface = when (loginStore.type) {
LoginType.LIBRUS -> Librus(app, profile, loginStore, taskCallback) LoginType.LIBRUS -> Librus(app, profile, loginStore, taskCallback)
LoginType.MOBIDZIENNIK -> Mobidziennik(app, profile, loginStore, taskCallback) LoginType.MOBIDZIENNIK -> Mobidziennik(app, profile, loginStore, taskCallback)
@ -126,7 +118,6 @@ 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,29 +52,28 @@ class ProfileArchiver(val app: App, val profile: Profile) {
when (profile.loginStoreType) { when (profile.loginStoreType) {
LoginType.LIBRUS -> { LoginType.LIBRUS -> {
profile.studentData.remove("isPremium") profile.removeStudentData("isPremium")
profile.studentData.remove("pushDeviceId") profile.removeStudentData("pushDeviceId")
profile.studentData.remove("startPointsSemester1") profile.removeStudentData("startPointsSemester1")
profile.studentData.remove("startPointsSemester2") profile.removeStudentData("startPointsSemester2")
profile.studentData.remove("enablePointGrades") profile.removeStudentData("enablePointGrades")
profile.studentData.remove("enableDescriptiveGrades") profile.removeStudentData("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.studentData.remove("currentSemesterEndDate") profile.removeStudentData("currentSemesterEndDate")
profile.studentData.remove("studentSemesterId") profile.removeStudentData("studentSemesterId")
profile.studentData.remove("studentSemesterNumber") profile.removeStudentData("studentSemesterNumber")
profile.studentData.remove("semester1Id") profile.removeStudentData("semester1Id")
profile.studentData.remove("semester2Id") profile.removeStudentData("semester2Id")
profile.studentData.remove("studentClassId") profile.removeStudentData("studentClassId")
} }
LoginType.IDZIENNIK -> { LoginType.IDZIENNIK -> {
profile.studentData.remove("schoolYearId") profile.removeStudentData("schoolYearId")
} }
LoginType.EDUDZIENNIK -> {} LoginType.EDUDZIENNIK -> {}
LoginType.PODLASIE -> {} LoginType.PODLASIE -> {}
LoginType.USOS -> {}
LoginType.DEMO -> {} LoginType.DEMO -> {}
LoginType.TEMPLATE -> {} LoginType.TEMPLATE -> {}
} }

View file

@ -1,84 +0,0 @@
/*
* 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,9 +10,7 @@ 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) {
@ -119,7 +117,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["accountLogin"] = value; mApiLogin = value } set(value) { profile?.putStudentData("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.
@ -128,7 +126,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["accountPassword"] = value; mApiPassword = value } set(value) { profile?.putStudentData("accountPassword", value); mApiPassword = value }
/** /**
* A JST login Code. * A JST login Code.
@ -137,7 +135,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["accountCode"] = value; mApiCode = value } set(value) { profile?.putStudentData("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.
@ -145,7 +143,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["accountPin"] = value; mApiPin = value } set(value) { profile?.putStudentData("accountPin", value); mApiPin = value }
/** /**
* A Synergia API access token. * A Synergia API access token.
@ -156,7 +154,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["accountToken"] = value ?: return; } set(value) { mApiAccessToken = value; profile?.putStudentData("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.
@ -164,7 +162,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["accountRefreshToken"] = value ?: return; } set(value) { mApiRefreshToken = value; profile?.putStudentData("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.
@ -173,7 +171,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["accountTokenTime"] = value; } set(value) { mApiTokenExpiryTime = value; profile?.putStudentData("accountTokenTime", value) ?: return; }
/** /**
* A push device ID, generated by Librus when registering * A push device ID, generated by Librus when registering
@ -183,7 +181,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["pushDeviceId"] = value; } set(value) { mPushDeviceId = value; profile?.putStudentData("pushDeviceId", value) ?: return; }
/* _____ _ /* _____ _
/ ____| (_) / ____| (_)
@ -200,7 +198,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["accountSID"] = value; mSynergiaSessionId = value } set(value) { profile?.putStudentData("accountSID", value) ?: return; 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.
@ -209,7 +207,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["accountSIDTime"] = value; mSynergiaSessionIdExpiryTime = value } set(value) { profile?.putStudentData("accountSIDTime", value) ?: return; mSynergiaSessionIdExpiryTime = value }
/** /**
@ -219,7 +217,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["messagesSID"] = value; mMessagesSessionId = value } set(value) { profile?.putStudentData("messagesSID", value) ?: return; 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.
@ -228,7 +226,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["messagesSIDTime"] = value; mMessagesSessionIdExpiryTime = value } set(value) { profile?.putStudentData("messagesSIDTime", value) ?: return; mMessagesSessionIdExpiryTime = value }
/* ____ _ _ /* ____ _ _
/ __ \| | | | / __ \| | | |
@ -238,42 +236,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["isPremium"] = value } set(value) { profile?.putStudentData("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["schoolName"] = value; mSchoolName = value } set(value) { profile?.putStudentData("schoolName", value) ?: return; 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["unitId"] = value; mUnitId = value } set(value) { profile?.putStudentData("unitId", value) ?: return; 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["startPointsSemester1"] = value; mStartPointsSemester1 = value } set(value) { profile?.putStudentData("startPointsSemester1", value) ?: return; 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["startPointsSemester2"] = value; mStartPointsSemester2 = value } set(value) { profile?.putStudentData("startPointsSemester2", value) ?: return; 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["enablePointGrades"] = value; mEnablePointGrades = value } set(value) { profile?.putStudentData("enablePointGrades", value) ?: return; 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["enableDescriptiveGrades"] = value; mEnableDescriptiveGrades = value } set(value) { profile?.putStudentData("enableDescriptiveGrades", value) ?: return; 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["timetableNotPublic"] = value; mTimetableNotPublic = value } set(value) { profile?.putStudentData("timetableNotPublic", value) ?: return; 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("Teacher")?.getLong("Id") ?: -1 val teacherId = note.getJsonObject("AddedBy")?.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,7 +190,6 @@ 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 = receiver.select("readed").text() val readDateText = message.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,7 +16,6 @@ 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,9 +24,6 @@ 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))
@ -36,7 +33,6 @@ 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()) {
@ -62,23 +58,18 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
} }
}} }}
private fun authorize(url: String, referer: String? = null) { private fun authorize(url: String?) {
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=([^&?]+)", Pattern.DOTALL or Pattern.MULTILINE).matcher(location) val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([A-z0-9]+?)$", Pattern.DOTALL or Pattern.MULTILINE).matcher(location)
when { when {
authMatcher.find() -> { authMatcher.find() -> {
accessToken(authMatcher.group(1), null) accessToken(authMatcher.group(1), null)
@ -92,31 +83,16 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
authorize(location) authorize(location)
} }
} }
return } else {
} val csrfMatcher = Pattern.compile("name=\"csrf-token\" content=\"([A-z0-9=+/\\-_]+?)\"", Pattern.DOTALL).matcher(text)
if (csrfMatcher.find()) {
if (checkError(text, response)) login(csrfMatcher.group(1) ?: "")
return } else {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_CSRF_MISSING)
var loginUrl = if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL .withResponse(response)
val csrfToken = Regexes.HTML_CSRF_TOKEN.find(text)?.get(1) ?: "" .withApiResponse(text))
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) {
@ -129,54 +105,8 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
.enqueue() .enqueue()
} }
private fun checkError(text: String, response: Response): Boolean { private fun login(csrfToken: String) {
when { d(TAG, "Request: Librus/Login/Portal - ${if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL}")
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)
@ -186,46 +116,67 @@ 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 : TextCallbackHandler() { .callback(object : JsonCallbackHandler() {
override fun onSuccess(text: String?, response: Response) { override fun onSuccess(json: JsonObject?, 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(text) .withApiResponse(json)
.withResponse(response))
return
}
if (text == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response)) .withResponse(response))
return return
} }
authorize( if (json == null) {
url = location if (response.parserErrorBody?.contains("wciąż nieaktywne") == true) {
?: if (data.fakeLogin) data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED)
FAKE_LIBRUS_AUTHORIZE .withResponse(response))
else return
LIBRUS_AUTHORIZE_URL, }
referer = referer, data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
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) {
data.requireUserAction(
type = UserActionRequiredEvent.Type.RECAPTCHA,
params = Bundle(
"siteKey" to LIBRUS_PORTAL_RECAPTCHA_KEY,
"referer" to LIBRUS_PORTAL_RECAPTCHA_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,9 +11,7 @@ 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
@ -87,7 +85,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["studentId"] = value; mStudentId = value } set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value }
/* __ __ _ /* __ __ _
\ \ / / | | \ \ / / | |
@ -127,7 +125,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["globalId"] = value; mGlobalId = value } set(value) { profile?.putStudentData("globalId", value) ?: return; mGlobalId = value }
private var mGlobalId: String? = null private var mGlobalId: String? = null
/** /**
@ -137,7 +135,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["email"] = value; mLoginEmail = value } set(value) { profile?.putStudentData("email", value); mLoginEmail = value }
private var mLoginEmail: String? = null private var mLoginEmail: String? = null
/** /**
@ -146,7 +144,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["loginId"] = value; mLoginId = value } set(value) { profile?.putStudentData("loginId", value) ?: return; mLoginId = value }
private var mLoginId: String? = null private var mLoginId: String? = null
/** /**
@ -154,7 +152,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["ciasteczkoAutoryzacji"] = value; mCiasteczkoAutoryzacji = value } set(value) { profile?.putStudentData("ciasteczkoAutoryzacji", value) ?: return; mCiasteczkoAutoryzacji = value }
private var mCiasteczkoAutoryzacji: String? = null private var mCiasteczkoAutoryzacji: String? = null

View file

@ -12,7 +12,6 @@ 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,7 +88,6 @@ 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,7 +10,6 @@ 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?,
@ -25,7 +24,6 @@ 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] }
if (email.isNotNullNorBlank())
data.loginEmail = email 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)

View file

@ -21,7 +21,6 @@ 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,7 +15,6 @@ 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,7 +336,6 @@ 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,7 +16,6 @@ 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
@ -78,9 +77,7 @@ class MobidziennikLoginApi2(val data: DataMobidziennik, val onSuccess: () -> Uni
} }
} }
val email = json.getString("email") data.loginEmail = 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,9 +10,7 @@ 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) {
@ -42,7 +40,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["apiUrl"] = value; mApiUrl = value } set(value) { profile?.putStudentData("apiUrl", value) ?: return; mApiUrl = value }
/* ____ _ _ /* ____ _ _
/ __ \| | | | / __ \| | | |
@ -53,32 +51,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["studentId"] = value; mStudentId = value } set(value) { profile?.putStudentData("studentId", value) ?: return; 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["studentLogin"] = value; mStudentLogin = value } set(value) { profile?.putStudentData("studentLogin", value) ?: return; 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["schoolName"] = value; mSchoolName = value } set(value) { profile?.putStudentData("schoolName", value) ?: return; 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["className"] = value; mClassName = value } set(value) { profile?.putStudentData("className", value) ?: return; 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["schoolYear"] = value; mSchoolYear = value } set(value) { profile?.putStudentData("schoolYear", value) ?: return; 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["currentSemester"] = value; mCurrentSemester = value } set(value) { profile?.putStudentData("currentSemester", value) ?: return; 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,7 +17,6 @@ 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,7 +72,6 @@ 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,9 +10,7 @@ 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
@ -45,12 +43,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["webCookie"] = value; mWebCookie = value } set(value) { profile?.putStudentData("webCookie", value) ?: return; 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["webExpiryTime"] = value; mWebExpiryTime = value } set(value) { profile?.putStudentData("webExpiryTime", value) ?: return; mWebExpiryTime = value }
/* _ /* _
/\ (_) /\ (_)
@ -63,10 +61,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["apiToken"] = value; mApiToken = value } set(value) { profile?.putStudentData("apiToken", value) ?: return; 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["apiExpiryTime"] = value; mApiExpiryTime = value } set(value) { profile?.putStudentData("apiExpiryTime", value) ?: return; mApiExpiryTime = value }
} }

View file

@ -4,14 +4,11 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.usos package pl.szczodrzynski.edziennik.data.api.edziennik.usos
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.models.Data 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,
@ -72,11 +69,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["studentId"] = value; mStudentId = value } set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value }
private var mStudentId: Int? = null private var mStudentId: Int? = null
var termNames: Map<String, String> = mapOf()
get() { mTermNames = mTermNames ?: profile?.getStudentData("termNames", null)?.let { app.gson.fromJson(it, field.toMutableMap()::class.java) }; return mTermNames ?: mapOf() }
set(value) { profile["termNames"] = app.gson.toJson(value); mTermNames = value }
private var mTermNames: Map<String, String>? = null
} }

View file

@ -14,8 +14,6 @@ const val ENDPOINT_USOS_API_USER = 7000
const val ENDPOINT_USOS_API_TERMS = 7010 const val ENDPOINT_USOS_API_TERMS = 7010
const val ENDPOINT_USOS_API_COURSES = 7020 const val ENDPOINT_USOS_API_COURSES = 7020
const val ENDPOINT_USOS_API_TIMETABLE = 7030 const val ENDPOINT_USOS_API_TIMETABLE = 7030
const val ENDPOINT_USOS_API_ECTS_POINTS = 7040
const val ENDPOINT_USOS_API_EXAM_REPORTS = 7050
val UsosFeatures = listOf( val UsosFeatures = listOf(
/* /*
@ -41,12 +39,4 @@ val UsosFeatures = listOf(
Feature(LoginType.USOS, FeatureType.TIMETABLE, listOf( Feature(LoginType.USOS, FeatureType.TIMETABLE, listOf(
ENDPOINT_USOS_API_TIMETABLE to LoginMethod.USOS_API, ENDPOINT_USOS_API_TIMETABLE to LoginMethod.USOS_API,
)), )),
/*
* Grades
*/
Feature(LoginType.USOS, FeatureType.GRADES, listOf(
ENDPOINT_USOS_API_ECTS_POINTS to LoginMethod.USOS_API,
ENDPOINT_USOS_API_EXAM_REPORTS to LoginMethod.USOS_API,
)),
) )

View file

@ -8,8 +8,6 @@ import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.* import pl.szczodrzynski.edziennik.data.api.edziennik.usos.*
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiEctsPoints
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiExamReports
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTerms import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTerms
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTimetable import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTimetable
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiUser import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiUser
@ -60,14 +58,6 @@ class UsosData(val data: DataUsos, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_timetable) data.startProgress(R.string.edziennik_progress_endpoint_timetable)
UsosApiTimetable(data, lastSync, onSuccess) UsosApiTimetable(data, lastSync, onSuccess)
} }
ENDPOINT_USOS_API_ECTS_POINTS -> {
data.startProgress(R.string.edziennik_progress_endpoint_grade_categories)
UsosApiEctsPoints(data, lastSync, onSuccess)
}
ENDPOINT_USOS_API_EXAM_REPORTS -> {
data.startProgress(R.string.edziennik_progress_endpoint_grades)
UsosApiExamReports(data, lastSync, onSuccess)
}
else -> onSuccess(endpointId) else -> onSuccess(endpointId)
} }
} }

View file

@ -9,7 +9,6 @@ import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory
import pl.szczodrzynski.edziennik.data.db.entity.Team import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ext.*
@ -26,20 +25,17 @@ class UsosApiCourses(
apiRequest<JsonObject>( apiRequest<JsonObject>(
tag = TAG, tag = TAG,
service = "courses/user", service = "courses/user",
params = mapOf(
"active_terms_only" to false,
),
fields = listOf( fields = listOf(
// "terms" to listOf("id", "name", "start_date", "end_date"), // "terms" to listOf("id", "name", "start_date", "end_date"),
"course_editions" to listOf( "course_editions" to listOf(
"course_id", "course_id",
"course_name", "course_name",
// "term_id",
"user_groups" to listOf( "user_groups" to listOf(
"course_unit_id", "course_unit_id",
"group_number", "group_number",
"class_type", // "class_type",
"class_type_id", "class_type_id",
"term_id",
"lecturers", "lecturers",
), ),
), ),
@ -67,38 +63,22 @@ class UsosApiCourses(
for (courseEdition in courseEditions) { for (courseEdition in courseEditions) {
val courseId = courseEdition.getString("course_id") ?: continue val courseId = courseEdition.getString("course_id") ?: continue
val courseName = courseEdition.getLangString("course_name") ?: continue val courseName = courseEdition.getLangString("course_name") ?: continue
val userGroups = val userGroups = courseEdition.getJsonArray("user_groups")?.asJsonObjectList() ?: continue
courseEdition.getJsonArray("user_groups")?.asJsonObjectList() ?: continue
for (userGroup in userGroups) { for (userGroup in userGroups) {
val courseUnitId = userGroup.getLong("course_unit_id") ?: continue val courseUnitId = userGroup.getLong("course_unit_id") ?: continue
val groupNumber = userGroup.getInt("group_number") ?: continue val groupNumber = userGroup.getInt("group_number") ?: continue
val classType = userGroup.getLangString("class_type") ?: continue // val classType = userGroup.getLangString("class_type") ?: continue
val classTypeId = userGroup.getString("class_type_id") ?: continue val classTypeId = userGroup.getString("class_type_id") ?: continue
val termId = userGroup.getString("term_id") ?: continue
val lecturers = userGroup.getLecturerIds("lecturers") val lecturers = userGroup.getLecturerIds("lecturers")
data.teamList.put( data.teamList.put(courseUnitId, Team(
courseUnitId, Team(
profileId, profileId,
courseUnitId, courseUnitId,
"${profile?.studentClassName} $courseName ($classTypeId$groupNumber)", "${profile?.studentClassName} $classTypeId$groupNumber - $courseName",
2, 2,
"${data.schoolId}:${termId}:${courseId} $classTypeId$groupNumber", "${data.schoolId}:${courseId} $classTypeId$groupNumber",
lecturers.firstOrNull() ?: -1L, lecturers.firstOrNull() ?: -1L,
) ))
)
val gradeCategory = data.gradeCategories[courseUnitId]
data.gradeCategories.put(
courseUnitId, GradeCategory(
profileId,
courseUnitId,
gradeCategory?.weight ?: -1.0f,
0,
courseId,
).addColumn(classType)
)
hasValidTeam = true hasValidTeam = true
} }
} }

View file

@ -1,56 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2025-1-31.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_ECTS_POINTS
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
import pl.szczodrzynski.edziennik.ext.DAY
import pl.szczodrzynski.edziennik.ext.filter
class UsosApiEctsPoints(
override val data: DataUsos,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit,
) : UsosApi(data, lastSync) {
companion object {
const val TAG = "UsosApiEctsPoints"
}
init {
apiRequest<JsonObject>(
tag = TAG,
service = "courses/user_ects_points",
responseType = ResponseType.OBJECT,
) { json, response ->
if (!processResponse(json)) {
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
return@apiRequest
}
data.setSyncNext(ENDPOINT_USOS_API_ECTS_POINTS, 2 * DAY)
onSuccess(ENDPOINT_USOS_API_ECTS_POINTS)
}
}
private fun processResponse(json: JsonObject): Boolean {
for ((_, coursePointsEl) in json.entrySet()) {
if (!coursePointsEl.isJsonObject)
continue
for ((courseId, pointsEl) in coursePointsEl.asJsonObject.entrySet()) {
if (!pointsEl.isJsonPrimitive)
continue
val gradeCategories = data.gradeCategories
.filter { it.text == courseId }
gradeCategories.forEach {
it.weight = pointsEl.asString.toFloatOrNull() ?: -1.0f
}
}
}
return true
}
}

View file

@ -1,212 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2025-1-31.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_EXAM_REPORTS
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NORMAL
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NO_GRADE
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
import pl.szczodrzynski.edziennik.ext.getBoolean
import pl.szczodrzynski.edziennik.ext.getInt
import pl.szczodrzynski.edziennik.ext.getJsonArray
import pl.szczodrzynski.edziennik.ext.getJsonObject
import pl.szczodrzynski.edziennik.ext.getLong
import pl.szczodrzynski.edziennik.ext.getString
import pl.szczodrzynski.edziennik.ext.join
import pl.szczodrzynski.edziennik.utils.models.Date
class UsosApiExamReports(
override val data: DataUsos,
override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit,
) : UsosApi(data, lastSync) {
companion object {
const val TAG = "UsosApiExamReports"
}
private val missingTermNames = mutableSetOf<String>()
init {
apiRequest<JsonObject>(
tag = TAG,
service = "examrep/user2",
fields = listOf(
"id",
"type_description",
"course_unit" to listOf("id", "course_name"),
"sessions" to listOf(
"number",
"description",
"issuer_grades" to listOf(
"value_symbol",
// "value_description",
"passes",
"counts_into_average",
"date_modified",
"modification_author",
"comment",
),
),
),
responseType = ResponseType.OBJECT,
) { json, response ->
if (!processResponse(json)) {
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
return@apiRequest
}
data.toRemove.add(DataRemoveModel.Grades.all())
data.setSyncNext(ENDPOINT_USOS_API_EXAM_REPORTS, SYNC_ALWAYS)
if (missingTermNames.isEmpty())
onSuccess(ENDPOINT_USOS_API_EXAM_REPORTS)
else
UsosApiTerms(data, lastSync, onSuccess, missingTermNames)
}
}
private fun processResponse(json: JsonObject): Boolean {
for ((termId, courseEditionEl) in json.entrySet()) {
if (!courseEditionEl.isJsonObject)
continue
for ((courseId, examReportsEl) in courseEditionEl.asJsonObject.entrySet()) {
if (!examReportsEl.isJsonArray)
continue
for (examReportEl in examReportsEl.asJsonArray) {
if (!examReportEl.isJsonObject)
continue
val examReport = examReportEl.asJsonObject
processExamReport(termId, courseId, examReport)
}
}
}
return true
}
private fun processExamReport(termId: String, courseId: String, examReport: JsonObject) {
val examId = examReport.getString("id")?.toIntOrNull()
?: return
val typeDescription = examReport.getLangString("type_description")
val courseUnit = examReport.getJsonObject("course_unit")
?: return
val courseUnitId = courseUnit.getString("id")?.toLongOrNull()
?: return
val courseName = courseUnit.getLangString("course_name")
?: return
val sessions = examReport.getJsonArray("sessions")
?: return
val gradeCategory = data.gradeCategories[courseUnitId]
val classType = gradeCategory?.columns?.get(0)
val subject = data.getSubject(
id = null,
name = courseName,
shortName = courseId,
)
var hasGrade = false
for (sessionEl in sessions) {
if (!sessionEl.isJsonObject)
continue
val session = sessionEl.asJsonObject
val sessionNumber = session.getInt("number") ?: continue
val sessionDescription = session.getLangString("description")
val issuerGrade = session.getJsonObject("issuer_grades")
val valueSymbol = issuerGrade.getString("value_symbol") ?: continue
val passes = issuerGrade.getBoolean("passes")
val countsIntoAverage = issuerGrade.getString("counts_into_average") ?: "T"
val dateModified = issuerGrade.getString("date_modified")
val modificationAuthorId = issuerGrade.getJsonObject("modification_author")
?.getLong("id") ?: -1L
val comment = issuerGrade.getString("comment")
val value = valueSymbol.toFloatOrNull() ?: 0.0f
if (termId !in data.termNames) {
missingTermNames.add(termId)
}
val gradeObject = Grade(
profileId = profileId,
id = examId * 10L + sessionNumber,
name = valueSymbol,
type = TYPE_NORMAL,
value = value,
weight = if (countsIntoAverage == "T") gradeCategory?.weight ?: 0.0f else 0.0f,
color = (if (passes == true) 0xFF465FB3 else 0xFFB71C1C).toInt(),
category = typeDescription,
description = listOfNotNull(classType, sessionDescription, comment).join(" - "),
comment = termId,
semester = 1,
teacherId = modificationAuthorId,
subjectId = subject.id,
addedDate = Date.fromIso(dateModified),
)
hasGrade = true
if (sessionNumber > 1) {
val origId = examId * 10L + sessionNumber - 1
val grades = data.gradeList.filter { it.id == origId }
val improvedGrade = grades.firstOrNull()
improvedGrade?.parentId = gradeObject.id
improvedGrade?.weight = 0.0f
gradeObject.isImprovement = true
}
data.gradeList.add(gradeObject)
data.metadataList.add(
Metadata(
profileId,
MetadataType.GRADE,
gradeObject.id,
profile?.empty ?: false,
profile?.empty ?: false,
)
)
}
if (!hasGrade) {
// add an "empty" grade for the exam
val gradeObject = Grade(
profileId = profileId,
id = examId * 10L,
name = "...",
type = TYPE_NO_GRADE,
value = 0.0f,
weight = 0.0f,
color = 0xFFBABABD.toInt(),
category = typeDescription,
description = classType,
comment = termId,
semester = 1,
teacherId = -1L,
subjectId = subject.id,
addedDate = 0,
)
data.gradeList.add(gradeObject)
data.metadataList.add(
Metadata(
profileId,
MetadataType.GRADE,
gradeObject.id,
true,
true,
)
)
}
}
}

View file

@ -5,7 +5,6 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos
import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS
@ -17,81 +16,43 @@ class UsosApiTerms(
override val data: DataUsos, override val data: DataUsos,
override val lastSync: Long?, override val lastSync: Long?,
val onSuccess: (endpointId: Int) -> Unit, val onSuccess: (endpointId: Int) -> Unit,
names: Set<String>? = null,
) : UsosApi(data, lastSync) { ) : UsosApi(data, lastSync) {
companion object { companion object {
const val TAG = "UsosApiTerms" const val TAG = "UsosApiTerms"
} }
init { init {
if (names != null) {
apiRequest<JsonObject>(
tag = TAG,
service = "terms/terms",
params = mapOf("term_ids" to names.joinToString("|")),
responseType = ResponseType.OBJECT,
) { json, response ->
if (!processResponse(json.entrySet().map { it.value.asJsonObject })) {
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
return@apiRequest
}
data.setSyncNext(ENDPOINT_USOS_API_TERMS, 2 * DAY)
onSuccess(ENDPOINT_USOS_API_TERMS)
}
} else {
apiRequest<JsonArray>( apiRequest<JsonArray>(
tag = TAG, tag = TAG,
service = "terms/search", service = "terms/search",
params = mapOf("query" to Date.getToday().year.toString()), params = mapOf(
"query" to Date.getToday().year.toString(),
),
responseType = ResponseType.ARRAY, responseType = ResponseType.ARRAY,
) { json, response -> ) { json, response ->
if (!processResponse(json.asJsonObjectList())) { if (!processResponse(json)) {
data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response)
return@apiRequest return@apiRequest
} }
data.setSyncNext(ENDPOINT_USOS_API_TERMS, 2 * DAY) data.setSyncNext(ENDPOINT_USOS_API_TERMS, 7 * DAY)
onSuccess(ENDPOINT_USOS_API_TERMS) onSuccess(ENDPOINT_USOS_API_TERMS)
} }
} }
}
private fun processResponse(terms: List<JsonObject>): Boolean { private fun processResponse(json: JsonArray): Boolean {
val profile = profile ?: return false
val termNames = data.termNames.toMutableMap()
val today = Date.getToday() val today = Date.getToday()
for (term in terms) { for (term in json.asJsonObjectList()) {
val id = term.getString("id")
val name = term.getLangString("name")
val orderKey = term.getInt("order_key")
if (id != null && name != null)
termNames[id] = "$orderKey$$name"
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) } ?: continue 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) } ?: continue val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) } ?: continue
if (today !in startDate..finishDate) if (today in startDate..finishDate) {
continue profile?.studentSchoolYearStart = startDate.year
profile?.dateSemester1Start = startDate
if (startDate.month >= 8) profile?.dateSemester2Start = finishDate
profile.dateSemester1Start = startDate }
else
profile.dateSemester2Start = startDate
if (finishDate.month >= 8)
profile.dateYearEnd = finishDate
else
profile.dateSemester2Start = finishDate
} }
// update school year start
profile.studentSchoolYearStart = profile.dateSemester1Start.year
// update year end date if there is a new year
if (profile.dateYearEnd <= profile.dateSemester1Start)
profile.dateYearEnd =
profile.dateSemester1Start.clone().setYear(profile.dateSemester1Start.year + 1)
data.termNames = termNames
return true return true
} }
} }

View file

@ -104,9 +104,8 @@ 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 = "Sala $roomNumber / bud. $buildingId ${groupName ?: ""}" it.classroom = "$buildingId / $roomNumber ${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

@ -32,8 +32,7 @@ class UsosApiUser(
"last_name", "last_name",
"student_number", "student_number",
"student_programmes" to listOf( "student_programmes" to listOf(
"id", "programme" to listOf("id"),
"programme" to listOf("id", "description"),
), ),
), ),
), ),
@ -41,11 +40,9 @@ class UsosApiUser(
) { json, response -> ) { json, response ->
val programmes = json.getJsonArray("student_programmes") val programmes = json.getJsonArray("student_programmes")
if (programmes.isNullOrEmpty()) { if (programmes.isNullOrEmpty()) {
data.error( data.error(ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES)
ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES)
.withApiResponse(json) .withApiResponse(json)
.withResponse(response) .withResponse(response))
)
return@apiRequest return@apiRequest
} }
@ -53,19 +50,13 @@ class UsosApiUser(
val lastName = json.getString("last_name") val lastName = json.getString("last_name")
val studentName = buildFullName(firstName, lastName) val studentName = buildFullName(firstName, lastName)
val studentProgrammeId = programmes.getJsonObject(0)
.getString("id")
val programmeId = programmes.getJsonObject(0)
.getJsonObject("programme")
.getString("id")
data.studentId = json.getInt("id") ?: data.studentId data.studentId = json.getInt("id") ?: data.studentId
profile?.studentNameLong = studentName profile?.studentNameLong = studentName
profile?.studentNameShort = studentName.getShortName() profile?.studentNameShort = studentName.getShortName()
profile?.studentNumber = json.getInt("student_number", -1) profile?.studentNumber = json.getInt("student_number", -1)
profile?.studentClassName = programmeId profile?.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id")
val team = programmeId?.let { profile?.studentClassName?.let {
data.getTeam( data.getTeam(
id = null, id = null,
name = it, name = it,
@ -73,7 +64,6 @@ class UsosApiUser(
isTeamClass = true, isTeamClass = true,
) )
} }
team?.code = "${data.schoolId}:${studentProgrammeId}:${programmeId}"
data.setSyncNext(ENDPOINT_USOS_API_USER, 4 * DAY) data.setSyncNext(ENDPOINT_USOS_API_USER, 4 * DAY)
onSuccess(ENDPOINT_USOS_API_USER) onSuccess(ENDPOINT_USOS_API_USER)

View file

@ -12,9 +12,7 @@ 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) {
@ -59,7 +57,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["symbol"] = value; mSymbol = value } set(value) { profile?.putStudentData("symbol", value); mSymbol = value }
/** /**
* Group symbol/number of the student's school. * Group symbol/number of the student's school.
@ -71,7 +69,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["schoolSymbol"] = value; mSchoolSymbol = value } set(value) { profile?.putStudentData("schoolSymbol", value) ?: return; mSchoolSymbol = value }
/** /**
* Short name of the school, used in some places. * Short name of the school, used in some places.
@ -81,7 +79,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["schoolShort"] = value; mSchoolShort = value } set(value) { profile?.putStudentData("schoolShort", value) ?: return; mSchoolShort = value }
/** /**
* A school code consisting of the [symbol] and [schoolSymbol]. * A school code consisting of the [symbol] and [schoolSymbol].
@ -93,7 +91,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["schoolName"] = value; mSchoolCode = value } set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolCode = value }
/** /**
* ID of the student. * ID of the student.
@ -103,7 +101,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["studentId"] = value; mStudentId = value } set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value }
/** /**
* ID of the student's account. * ID of the student's account.
@ -113,7 +111,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["studentLoginId"] = value; mStudentLoginId = value } set(value) { profile?.putStudentData("studentLoginId", value) ?: return; mStudentLoginId = value }
/** /**
* ID of the student's class. * ID of the student's class.
@ -123,7 +121,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["studentClassId"] = value; mStudentClassId = value } set(value) { profile?.putStudentData("studentClassId", value) ?: return; mStudentClassId = value }
/** /**
* ListaUczniow/IdOkresKlasyfikacyjny, e.g. 321 * ListaUczniow/IdOkresKlasyfikacyjny, e.g. 321
@ -131,26 +129,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["studentSemesterId"] = value; mStudentSemesterId = value } set(value) { profile?.putStudentData("studentSemesterId", value) ?: return; 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["studentUnitId"] = value; mStudentUnitId = value } set(value) { profile?.putStudentData("studentUnitId", value) ?: return; 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["studentConstituentId"] = value; mStudentConstituentId = value } set(value) { profile?.putStudentData("studentConstituentId", value) ?: return; 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["semester1Id"] = value; mSemester1Id = value } set(value) { profile?.putStudentData("semester1Id", value) ?: return; 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["semester2Id"] = value; mSemester2Id = value } set(value) { profile?.putStudentData("semester2Id", value) ?: return; mSemester2Id = value }
/** /**
* ListaUczniow/OkresNumer, e.g. 1 or 2 * ListaUczniow/OkresNumer, e.g. 1 or 2
@ -158,7 +156,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["studentSemesterNumber"] = value; mStudentSemesterNumber = value } set(value) { profile?.putStudentData("studentSemesterNumber", value) ?: return; mStudentSemesterNumber = value }
/** /**
* Date of the end of the current semester ([studentSemesterNumber]). * Date of the end of the current semester ([studentSemesterNumber]).
@ -168,7 +166,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["currentSemesterEndDate"] = value; mCurrentSemesterEndDate = value } set(value) { profile?.putStudentData("currentSemesterEndDate", value) ?: return; mCurrentSemesterEndDate = value }
/* _____ _____ ____ /* _____ _____ ____
/\ | __ \_ _| |___ \ /\ | __ \_ _| |___ \
@ -221,17 +219,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["hebeContext"] = value; mHebeContext = value } set(value) { profile?.putStudentData("hebeContext", value) ?: return; 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["messageBoxKey"] = value; mMessageBoxKey = value } set(value) { profile?.putStudentData("messageBoxKey", value) ?: return; 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["messageBoxName"] = value; mMessageBoxName = value } set(value) { profile?.putStudentData("messageBoxName", value) ?: return; mMessageBoxName = value }
val apiUrl: String? val apiUrl: String?
get() { get() {

View file

@ -26,7 +26,6 @@ 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
@ -184,7 +183,6 @@ 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"
@ -297,19 +295,6 @@ 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)
@ -353,7 +338,6 @@ 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 {
@ -364,7 +348,6 @@ 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
) )
} }
@ -399,7 +382,6 @@ 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)
@ -445,8 +427,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, allow404 = allow404) { json: JsonArray?, response -> apiGet(tag, url, query) { json: JsonArray, response ->
onSuccess(json?.map { it.asJsonObject } ?: listOf(), response) onSuccess(json.map { it.asJsonObject }, response)
} }
} }
} }

View file

@ -19,7 +19,6 @@ 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,
@ -42,15 +41,8 @@ class VulcanHebeAddressbook(
VULCAN_HEBE_ENDPOINT_ADDRESSBOOK, VULCAN_HEBE_ENDPOINT_ADDRESSBOOK,
HebeFilterType.BY_PERSON, HebeFilterType.BY_PERSON,
lastSync = lastSync, lastSync = lastSync,
includeFilterType = false, includeFilterType = false
allow404 = true, ) { list, _ ->
) { 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,9 +247,7 @@ 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

@ -198,6 +198,7 @@ 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,
@ -206,9 +207,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 == profile.config.hash && app.config.hash != "invalid") if (hash == config.hash)
return@mapNotNull null return@mapNotNull null
return@mapNotNull user to profile.config return@mapNotNull user to config
} }
val response = api.serverSync(ServerSyncRequest( val response = api.serverSync(ServerSyncRequest(
@ -257,10 +258,12 @@ class SzkolnyApi(val app: App) : CoroutineScope {
seen = profile.empty seen = profile.empty
notified = profile.empty notified = profile.empty
sharedBy = if (profile.userCode == event.sharedBy) if (profile.userCode == event.sharedBy) {
"self" sharedBy = "self"
else addedManually = true
eventSharedBy } else {
sharedBy = eventSharedBy
}
} }
} }
} }
@ -324,14 +327,11 @@ class SzkolnyApi(val app: App) : CoroutineScope {
} }
@Throws(Exception::class) @Throws(Exception::class)
fun shareNote(note: Note, teamId: Long? = null) { fun shareNote(note: Note) {
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 = if (teamId == null) val team = app.db.teamDao().getClassNow(note.profileId)
app.db.teamDao().getClassNow(note.profileId) ?: throw NullPointerException("TeamClass is not found")
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,

View file

@ -29,12 +29,11 @@ class SignatureInterceptor(val app: App) : Interceptor {
return chain.proceed( return chain.proceed(
request.newBuilder() request.newBuilder()
.header("X-ApiKey", app.config.apiKeyCustom?.takeValue() ?: 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.MTIzNDU2Nzg5MD4QLcHOGI===.$param2".sha256() return "$param1.MTIzNDU2Nzg5MD7hH6ZvtD===.$param2".sha256()
} }
} }

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[it].sync.notificationFilter } .map { app.config.getFor(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 = 100) ], version = 99)
@TypeConverters( @TypeConverters(
ConverterTime::class, ConverterTime::class,
ConverterDate::class, ConverterDate::class,
@ -188,7 +188,6 @@ abstract class AppDb : RoomDatabase() {
Migration97(), Migration97(),
Migration98(), Migration98(),
Migration99(), Migration99(),
Migration100(),
).allowMainThreadQueries().build() ).allowMainThreadQueries().build()
} }
} }

View file

@ -5,37 +5,13 @@
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 fromFeatureType(value: FeatureType?) = value?.id fun fromEnum(value: Enum<*>?) = value?.toInt()
@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

@ -24,7 +24,4 @@ interface EndpointTimerDao {
@Query("DELETE FROM endpointTimers WHERE profileId = :profileId") @Query("DELETE FROM endpointTimers WHERE profileId = :profileId")
fun clear(profileId: Int) fun clear(profileId: Int)
@Query("DELETE FROM endpointTimers")
fun clear()
} }

View file

@ -25,9 +25,6 @@ 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?
@ -46,7 +43,7 @@ abstract class EventTypeDao {
val typeList = data.eventTypes.map { val typeList = data.eventTypes.map {
EventType( EventType(
profileId = profile.id, profileId = profile.id,
id = it.id, id = it.id.toLong(),
name = it.name, name = it.name,
color = Color.parseColor(it.color), color = Color.parseColor(it.color),
order = order++, order = order++,
@ -56,21 +53,4 @@ abstract class EventTypeDao {
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,9 +28,6 @@ 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>>
@ -52,7 +49,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) @Query("SELECT profiles.* FROM teams JOIN profiles USING(profileId) WHERE teamCode = :teamCode AND registration = " + Profile.REGISTRATION_ENABLED + " AND enableSharedEvents = 1")
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,9 +107,6 @@ 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,22 +73,13 @@ 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 || isShared get() = field || sharedBy == "self"
/**
* 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")
@ -113,27 +104,6 @@ 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
@ -164,6 +134,9 @@ 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,6 +5,31 @@ 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",
@ -30,5 +55,35 @@ 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

@ -55,7 +55,6 @@ open class Grade(
const val TYPE_DESCRIPTIVE = 30 const val TYPE_DESCRIPTIVE = 30
const val TYPE_DESCRIPTIVE_TEXT = 31 const val TYPE_DESCRIPTIVE_TEXT = 31
const val TYPE_TEXT = 40 const val TYPE_TEXT = 40
const val TYPE_NO_GRADE = 100
} }
@ColumnInfo(name = "gradeValueMax") @ColumnInfo(name = "gradeValueMax")

View file

@ -50,13 +50,6 @@ 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)
@ -76,12 +69,7 @@ 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 = fun buildId(): Long = (displayDate?.combineWith(displayStartTime) ?: 0L) / 6L * 10L + (hashCode() and 0xFFFF)
(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,7 +9,6 @@ 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,18 +5,25 @@
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.dateToSemester import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.ext.getDrawable import pl.szczodrzynski.edziennik.utils.ProfileImageHolder
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(
@ -53,13 +60,9 @@ 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
@ -67,35 +70,117 @@ 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
val currentSemester fun hasStudentData(key: String) = studentData.has(key)
get() = dateToSemester(Date.getToday()) fun getStudentData(key: String, defaultValue: Boolean) = studentData.getBoolean(key) ?: defaultValue
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
get() = loginStoreType.name.lowercase() @Ignore
val registerName = loginStoreType.name.lowercase()
val canShare val canShare
get() = registration == REGISTRATION_ENABLED && !archived get() = registration == REGISTRATION_ENABLED && !archived
@delegate:Ignore override fun getImageDrawable(context: Context): Drawable {
@delegate:Transient if (archived) {
val config by lazy { App.config[this.id] } return context.getDrawableFromRes(pl.szczodrzynski.edziennik.R.drawable.profile_archived).also {
it.colorFilter = PorterDuffColorFilter(colorFromName(name), PorterDuff.Mode.DST_OVER)
}
}
override fun getImageDrawable(context: Context) = this.getDrawable(context) if (!image.isNullOrEmpty()) {
override fun getImageHolder(context: Context) = this.getHolder() try {
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,8 +6,6 @@ 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,
@ -28,7 +26,7 @@ enum class LoginMethod(
MOBIDZIENNIK_API2( MOBIDZIENNIK_API2(
loginType = LoginType.MOBIDZIENNIK, loginType = LoginType.MOBIDZIENNIK,
id = 1300, id = 1300,
isPossible = { profile, _ -> profile?.studentData?.getString("email").isNotNullNorBlank() }, isPossible = { profile, _ -> profile?.studentData?.has("email") ?: false },
), ),
LIBRUS_PORTAL( LIBRUS_PORTAL(
loginType = LoginType.LIBRUS, loginType = LoginType.LIBRUS,
@ -59,7 +57,7 @@ enum class LoginMethod(
VULCAN_WEB_MAIN( VULCAN_WEB_MAIN(
loginType = LoginType.VULCAN, loginType = LoginType.VULCAN,
id = 4100, id = 4100,
isPossible = { _, loginStore -> loginStore.getLoginData("webHost", null).isNotNullNorBlank() }, isPossible = { _, loginStore -> loginStore.hasLoginData("webHost") },
), ),
VULCAN_HEBE( VULCAN_HEBE(
loginType = LoginType.VULCAN, loginType = LoginType.VULCAN,

View file

@ -17,5 +17,4 @@ 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

@ -14,7 +14,7 @@ enum class LoginType(
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, schoolType = SchoolType.UNIVERSITY), USOS(id = 7, features = FEATURES_USOS, schoolType = SchoolType.UNIVERSITY),
DEMO(id = 8, features = setOf()), DEMO(id = 20, features = setOf()),
TEMPLATE(id = 21, features = setOf()), TEMPLATE(id = 21, features = setOf()),
// the graveyard // the graveyard

View file

@ -93,7 +93,6 @@ internal val FEATURES_PODLASIE = setOf(
internal val FEATURES_USOS = setOf( internal val FEATURES_USOS = setOf(
TIMETABLE, TIMETABLE,
AGENDA, AGENDA,
GRADES,
STUDENT_INFO, STUDENT_INFO,
STUDENT_NUMBER, STUDENT_NUMBER,

View file

@ -9,7 +9,6 @@ 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
@ -119,5 +118,4 @@ 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,7 +9,6 @@ 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(
@ -138,10 +137,9 @@ class LessonFull(
var seen: Boolean = false var seen: Boolean = false
var notified: Boolean = false var notified: Boolean = false
@Relation(parentColumn = "ownerId", entityColumn = "noteOwnerId", entity = Note::class) @Relation(parentColumn = "id", 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() = ownerId override fun getNoteOwnerId() = id
override fun getNoteShareTeamId() = teamId.takePositive()
} }

View file

@ -1,25 +0,0 @@
/*
* 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

@ -160,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(
@ -176,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[event.profileId].sync.notificationFilter val notificationFilter = app.config.getFor(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(
@ -211,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[team.profileId].sync.notificationFilter val notificationFilter = app.config.getFor(team.profileId).sync.notificationFilter
if (!notificationFilter.contains(NotificationType.REMOVED_SHARED_EVENT)) { if (!notificationFilter.contains(NotificationType.REMOVED_SHARED_EVENT)) {
val notification = Notification( val notification = Notification(
@ -265,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[note.profileId].sync.notificationFilter val notificationFilter = app.config.getFor(note.profileId).sync.notificationFilter
if (!notificationFilter.contains(type) && note.sharedBy != "self") { if (!notificationFilter.contains(type) && note.sharedBy != "self") {
val notification = Notification( val notification = Notification(

View file

@ -6,37 +6,12 @@ package pl.szczodrzynski.edziennik.data.firebase
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_ANNOUNCEMENTS import pl.szczodrzynski.edziennik.data.api.edziennik.librus.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_ATTENDANCES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_CATEGORIES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_COMMENTS
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_CLASS_FREE_DAYS
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADE_CATEGORIES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_EVENTS
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_HOMEWORK
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_LUCKY_NUMBER
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADE_CATEGORIES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NOTICES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_POINT_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_POINT_GRADE_CATEGORIES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_PT_MEETINGS
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_SCHOOL_FREE_DAYS
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TEXT_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TIMETABLES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_MESSAGES_RECEIVED
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK
import pl.szczodrzynski.edziennik.data.api.task.IApiTask import pl.szczodrzynski.edziennik.data.api.task.IApiTask
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.data.db.enums.LoginType
import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.ext.getString
import pl.szczodrzynski.edziennik.ext.getStudentData
class SzkolnyLibrusFirebase(val app: App, val profiles: List<Profile>, val message: FirebaseService.Message) { class SzkolnyLibrusFirebase(val app: App, val profiles: List<Profile>, val message: FirebaseService.Message) {
/*{ /*{

View file

@ -12,7 +12,6 @@ import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
import pl.szczodrzynski.edziennik.data.db.enums.LoginType import pl.szczodrzynski.edziennik.data.db.enums.LoginType
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.getStudentData
class SzkolnyMobidziennikFirebase(val app: App, val profiles: List<Profile>, val message: FirebaseService.Message) { class SzkolnyMobidziennikFirebase(val app: App, val profiles: List<Profile>, val message: FirebaseService.Message) {
/*{ /*{

View file

@ -4,15 +4,15 @@
package pl.szczodrzynski.edziennik.data.firebase package pl.szczodrzynski.edziennik.data.firebase
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.task.IApiTask import pl.szczodrzynski.edziennik.data.api.task.IApiTask
import pl.szczodrzynski.edziennik.data.db.entity.Message
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.data.db.enums.LoginType
import pl.szczodrzynski.edziennik.ext.getInt import pl.szczodrzynski.edziennik.ext.getInt
import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.ext.getString
import pl.szczodrzynski.edziennik.ext.getStudentData
import pl.szczodrzynski.edziennik.ext.toJsonObject import pl.szczodrzynski.edziennik.ext.toJsonObject
class SzkolnyVulcanFirebase(val app: App, val profiles: List<Profile>, val message: FirebaseService.Message) { class SzkolnyVulcanFirebase(val app: App, val profiles: List<Profile>, val message: FirebaseService.Message) {

View file

@ -6,9 +6,14 @@ package pl.szczodrzynski.edziennik.ext
import android.util.LongSparseArray import android.util.LongSparseArray
import androidx.core.util.forEach import androidx.core.util.forEach
import com.google.android.material.datepicker.CalendarConstraints
import com.google.gson.JsonElement
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.config.AppData
import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.entity.Team import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
fun List<Teacher>.byId(id: Long) = firstOrNull { it.id == id } fun List<Teacher>.byId(id: Long) = firstOrNull { it.id == id }
fun List<Teacher>.byNameFirstLast(nameFirstLast: String) = firstOrNull { it.name + " " + it.surname == nameFirstLast } fun List<Teacher>.byNameFirstLast(nameFirstLast: String) = firstOrNull { it.name + " " + it.surname == nameFirstLast }
@ -29,3 +34,22 @@ fun LongSparseArray<Team>.getById(id: Long): Team? {
} }
return null return null
} }
operator fun Profile.set(key: String, value: JsonElement) = this.studentData.add(key, value)
operator fun Profile.set(key: String, value: Boolean) = this.studentData.addProperty(key, value)
operator fun Profile.set(key: String, value: String?) = this.studentData.addProperty(key, value)
operator fun Profile.set(key: String, value: Number) = this.studentData.addProperty(key, value)
operator fun Profile.set(key: String, value: Char) = this.studentData.addProperty(key, value)
fun Profile.getSchoolYearConstrains(): CalendarConstraints {
return CalendarConstraints.Builder()
.setStart(dateSemester1Start.inMillisUtc)
.setEnd(dateYearEnd.inMillisUtc)
.build()
}
fun Profile.hasFeature(featureType: FeatureType) = featureType in this.loginStoreType.features
fun Profile.hasUIFeature(featureType: FeatureType) = featureType.isUIAlwaysAvailable || hasFeature(featureType)
fun Profile.getAppData() =
if (App.profileId == this.id) App.data else AppData.get(this.loginStoreType)

View file

@ -5,7 +5,6 @@
package pl.szczodrzynski.edziennik.ext package pl.szczodrzynski.edziennik.ext
import android.os.Bundle import android.os.Bundle
import com.google.gson.Gson
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
@ -49,11 +48,6 @@ fun JsonObject.putEnum(key: String, value: Enum<*>) = addProperty(key, value.toI
fun String.toJsonObject(): JsonObject? = try { JsonParser.parseString(this).asJsonObject } catch (ignore: Exception) { null } fun String.toJsonObject(): JsonObject? = try { JsonParser.parseString(this).asJsonObject } catch (ignore: Exception) { null }
fun String.toJsonArray(): JsonArray? = try { JsonParser.parseString(this).asJsonArray } catch (ignore: Exception) { null } fun String.toJsonArray(): JsonArray? = try { JsonParser.parseString(this).asJsonArray } catch (ignore: Exception) { null }
fun Any?.toJsonElement(): JsonElement = when (this) {
is Collection<*> -> JsonArray(this)
else -> Gson().toJsonTree(this)
}
operator fun JsonObject.set(key: String, value: JsonElement) = this.add(key, value) operator fun JsonObject.set(key: String, value: JsonElement) = this.add(key, value)
operator fun JsonObject.set(key: String, value: Boolean) = this.addProperty(key, value) operator fun JsonObject.set(key: String, value: Boolean) = this.addProperty(key, value)
operator fun JsonObject.set(key: String, value: String?) = this.addProperty(key, value) operator fun JsonObject.set(key: String, value: String?) = this.addProperty(key, value)
@ -73,8 +67,6 @@ fun JsonObject(vararg properties: Pair<String, Any?>): JsonObject {
is Number -> addProperty(key, value) is Number -> addProperty(key, value)
is Boolean -> addProperty(key, value) is Boolean -> addProperty(key, value)
is Enum<*> -> addProperty(key, value.toInt()) is Enum<*> -> addProperty(key, value.toInt())
null -> add(key, null)
else -> add(key, value.toJsonElement())
} }
} }
} }
@ -106,8 +98,6 @@ fun JsonArray(properties: Collection<Any?>): JsonArray {
is Char -> add(property as Char?) is Char -> add(property as Char?)
is Number -> add(property as Number?) is Number -> add(property as Number?)
is Boolean -> add(property as Boolean?) is Boolean -> add(property as Boolean?)
is Enum<*> -> add(property.toInt())
else -> add(property.toJsonElement())
} }
} }
} }

View file

@ -73,18 +73,9 @@ fun pendingIntentFlag(): Int {
return 0 return 0
} }
fun pendingIntentMutable(): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
return PendingIntent.FLAG_MUTABLE
return 0
}
fun Int?.takeValue() = if (this == -1) null else this fun Int?.takeValue() = if (this == -1) null else this
fun Int?.takePositive() = if (this == -1 || this == 0) null else this fun Int?.takePositive() = if (this == -1 || this == 0) null else this
fun Long?.takeValue() = if (this == -1L) null else this
fun Long?.takePositive() = if (this == -1L || this == 0L) null else this
fun String?.takeValue() = if (this.isNullOrBlank()) null else this fun String?.takeValue() = if (this.isNullOrBlank()) null else this
fun Any?.ignore() = Unit fun Any?.ignore() = Unit

View file

@ -1,129 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2022-10-25.
*/
package pl.szczodrzynski.edziennik.ext
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import com.google.android.material.datepicker.CalendarConstraints
import com.google.gson.JsonElement
import pl.droidsonroids.gif.GifDrawable
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.config.AppData
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
import pl.szczodrzynski.edziennik.data.db.enums.LoginType
import pl.szczodrzynski.edziennik.utils.ProfileImageHolder
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.ImageHolder
import pl.szczodrzynski.navlib.getDrawableFromRes
// TODO refactor Data* fields and make the receiver non-nullable
operator fun Profile?.set(key: String, value: JsonElement) = this?.studentData?.add(key, value)
operator fun Profile?.set(key: String, value: Boolean) = this?.studentData?.addProperty(key, value)
operator fun Profile?.set(key: String, value: String?) = this?.studentData?.addProperty(key, value)
operator fun Profile?.set(key: String, value: Number) = this?.studentData?.addProperty(key, value)
operator fun Profile?.set(key: String, value: Char) = this?.studentData?.addProperty(key, value)
fun Profile.getStudentData(key: String, defaultValue: Boolean) =
studentData.getBoolean(key) ?: defaultValue
fun Profile.getStudentData(key: String, defaultValue: String?) =
studentData.getString(key) ?: defaultValue
fun Profile.getStudentData(key: String, defaultValue: Int) =
studentData.getInt(key) ?: defaultValue
fun Profile.getStudentData(key: String, defaultValue: Long) =
studentData.getLong(key) ?: defaultValue
fun Profile.getStudentData(key: String, defaultValue: Float) =
studentData.getFloat(key) ?: defaultValue
fun Profile.getStudentData(key: String, defaultValue: Char) =
studentData.getChar(key) ?: defaultValue
fun Profile.getSemesterStart(semester: Int) =
if (semester == 1) dateSemester1Start else dateSemester2Start
fun Profile.getSemesterEnd(semester: Int) =
if (semester == 1) dateSemester2Start.clone().stepForward(0, 0, -1) else dateYearEnd
fun Profile.dateToSemester(date: Date) = if (date >= dateSemester2Start) 2 else 1
fun Profile.isBeforeYear() = false && Date.getToday() < dateSemester1Start
fun Profile.getSchoolYearConstrains(): CalendarConstraints {
return CalendarConstraints.Builder()
.setStart(dateSemester1Start.inMillisUtc)
.setEnd(dateYearEnd.inMillisUtc)
.build()
}
fun Profile.hasFeature(featureType: FeatureType) = featureType in this.loginStoreType.features
fun Profile.hasUIFeature(featureType: FeatureType) =
featureType.isUIAlwaysAvailable || hasFeature(featureType)
fun Profile.getAppData() =
if (App.profileId == this.id) App.data else AppData.get(this.loginStoreType)
fun Profile.shouldArchive(): Boolean {
return false
// 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 Profile.getDrawable(context: Context): Drawable {
if (archived) {
return context.getDrawableFromRes(R.drawable.profile_archived).also {
it.colorFilter = PorterDuffColorFilter(colorFromName(name), PorterDuff.Mode.DST_OVER)
}
}
if (!image.isNullOrEmpty()) {
try {
return if (image?.endsWith(".gif", true) == true) {
GifDrawable(image ?: "")
} else {
RoundedBitmapDrawableFactory.create(context.resources, image ?: "")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
return context.getDrawableFromRes(R.drawable.profile).also {
it.colorFilter = PorterDuffColorFilter(colorFromName(name), PorterDuff.Mode.DST_OVER)
}
}
fun Profile.getHolder(): ImageHolder {
if (archived) {
return ImageHolder(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))
}
}

View file

@ -15,7 +15,6 @@ import android.text.style.CharacterStyle
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.text.style.StrikethroughSpan import android.text.style.StrikethroughSpan
import android.text.style.StyleSpan import android.text.style.StyleSpan
import android.text.style.UnderlineSpan
import androidx.annotation.PluralsRes import androidx.annotation.PluralsRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.mikepenz.materialdrawer.holder.StringHolder import com.mikepenz.materialdrawer.holder.StringHolder
@ -161,11 +160,6 @@ fun CharSequence?.asBoldSpannable(): Spannable {
spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return spannable return spannable
} }
fun CharSequence?.asUnderlineSpannable(): Spannable {
val spannable = SpannableString(this)
spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return spannable
}
fun CharSequence.asSpannable( fun CharSequence.asSpannable(
vararg spans: CharacterStyle, vararg spans: CharacterStyle,
substring: CharSequence? = null, substring: CharSequence? = null,

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