Compare commits

...

54 Commits

Author SHA1 Message Date
4763033f24 [4.0-beta.6] Update build.gradle, signing and changelog. 2020-01-28 22:53:52 +01:00
3b0570d21c Revert "[Sync] Lower the priority of sync notification."
This reverts commit 1677be9e6fa30478564df94201ea0f8193d6e5b8.
2020-01-28 22:47:59 +01:00
16bf478d1a [UI/Agenda] Rewrite agenda in Kotlin and add lesson change counters. 2020-01-26 22:20:46 +01:00
5bf181b6d1 [Feedback] Implement notifications. 2020-01-26 22:03:20 +01:00
21b2e5d194 [Feedback] Add new feedback fragment and API. 2020-01-26 20:05:32 +01:00
759afcf3ca [Database/Migrations] Move migrations to files. 2020-01-25 17:23:47 +01:00
d48c7844a4 [UI/Settings] Add grades config dialog to settings fragment. 2020-01-25 13:31:40 +01:00
7d8caa8df7 [API/Librus] Use added by teacher id instead of lesson teacher in attendance. 2020-01-25 12:42:03 +01:00
62f53930da [Dialog/GradesConfig] Add grades view config dialog. 2020-01-22 23:09:07 +01:00
9a45cbb679 [UI] Add keepScreenOn in counter activity and bell sync dialog. 2020-01-22 23:08:23 +01:00
8e5a10f6d8 [API/Mobidziennik] Implement getting email for push registration. 2020-01-21 20:44:49 +01:00
10c57d2272 [Sync] Lower the priority of sync notification. 2020-01-20 23:10:35 +01:00
67d4d0f898 [Sync] Fix doubled and dead notifications during sync. 2020-01-20 21:36:53 +01:00
97e0d04842 [API] Partially revert "Include device object in each request." 2020-01-20 21:34:22 +01:00
3ba30ede92 [Sync] Fix sync notification crashing on Oreo+. 2020-01-20 20:26:52 +01:00
1035e411ab [API] Include device object in each request. 2020-01-20 19:43:14 +01:00
d5ae4b7ec9 [Home/Grades] Remove filtering grades by semester. 2020-01-20 19:30:20 +01:00
111d040cf9 [Updates] Fix no update toast not visible. 2020-01-20 19:27:59 +01:00
8cc594d170 [Widget/Timetable] Fix widget crashing with NO_LESSONS item. 2020-01-20 19:27:06 +01:00
d8a8bed68d [4.0-beta.5] Update build.gradle and signing. 2020-01-19 22:42:29 +01:00
eedbd954bd [Updates] Add toast for error while checking and for no updates. 2020-01-19 22:35:12 +01:00
0eb8366027 [Changelog] Fix changelog dialog appearance on Android N+. 2020-01-19 22:29:34 +01:00
894135104b [4.0-beta.4] Update build.gradle, signing and changelog. 2020-01-19 22:18:20 +01:00
7b2e408efc [Sync] Make sync not possible for archived profiles. Translate some error codes. 2020-01-19 22:17:57 +01:00
e4115c122e [Errors] Add reporting app version name along with the error. 2020-01-19 21:57:17 +01:00
537b16949e [Updates] Fix running update worker in Java. 2020-01-19 21:52:08 +01:00
ca60ceb2a7 [Firebase] Implement handling app updates. 2020-01-19 21:49:58 +01:00
0fad12fea5 [Updates] Change update channel to beta. 2020-01-19 21:39:28 +01:00
6cd2c23aac [Firebase] Implement handling server messages. 2020-01-19 21:31:37 +01:00
512baaa43f [Errors] Include parser error body when reporting HTTP errors. 2020-01-19 20:34:10 +01:00
d097fcc973 [API/Librus] Fix classrooms name short extraction when name contains two spaces. 2020-01-19 20:22:41 +01:00
621dbd459c [API/Vulcan] Fix marking messages as seen. 2020-01-19 20:03:25 +01:00
840ab4b0c4 [Models] Remove unused models and classes. 2020-01-19 19:30:38 +01:00
904be34a87 [Notifications] Fix showing an empty notification where the list is empty. 2020-01-19 19:19:30 +01:00
b7fc6fcc38 [Structure] Refactor App class to Kotlin. Rewrite SzkolnyTask and posting notifications. Remove dependency on AppConfig. Update libraries and gradle. 2020-01-19 19:07:27 +01:00
55c6e40d6d [DB] Convert AppDb to Kotlin. 2020-01-19 18:44:57 +01:00
4dfb015057 [Firebase/Librus] Implement basic push integration. 2020-01-19 01:17:33 +01:00
e40a0ba2bb [Strings] Add missing translations. 2020-01-18 00:44:13 +01:00
fd48f10df9 [Dialog/EventDetails] Show toast when calendar app not found instead of crashing. 2020-01-18 00:26:26 +01:00
6a54e7fef7 [Firebase] Implement Mobidziennik push service. 2020-01-16 09:27:30 +01:00
5c4d6ed140 [API/Edudziennik] Fix getting attendances. 2020-01-15 23:19:33 +01:00
9ed1be3594 [UI/DrawerProfiles] Add button for marking everything as read in every profile. 2020-01-15 22:58:22 +01:00
c5ce582678 [API/Edudziennik] Fix fix for semesters and getting grades on first login. 2020-01-15 22:26:38 +01:00
2050083bce [API/Edudziennik] Fix semesters and getting grades on first login. 2020-01-15 21:19:48 +01:00
92e6bdb562 [API/iDziennik] Fix regex getting school year in first login. 2020-01-13 22:42:30 +01:00
93e70c38b7 [Firebase] Implement base for per-register FCM tasks. 2020-01-12 21:39:06 +01:00
45b96179a5 [DB] Fix timetable migration crashing app. 2020-01-12 21:33:46 +01:00
a29a534a40 [API/Edudziennik] Fix showing notifications for presence attendances. 2020-01-12 19:45:20 +01:00
8e2297359c [Firebase] Implement new custom FCM service. 2020-01-11 19:07:25 +01:00
92ba7248ef [UI] Fix vector drawables crashing on API < 21. 2020-01-11 13:56:09 +01:00
f657d37cbd [DB/Timetable] Fix migration fixing wrong primary key columns. 2020-01-10 22:23:14 +01:00
9e312f60bf [APIService] Fix showing notification with no service running. 2020-01-10 21:34:55 +01:00
85f72b78f7 [DB/Timetable] Fix wrong primary key columns. 2020-01-10 21:33:59 +01:00
40acb67ceb [Errors] Fix Timeout error detection (SocketTimeoutException inherits from InterruptedIOException). 2020-01-10 16:44:54 +01:00
281 changed files with 6284 additions and 6287 deletions

View File

@ -2,7 +2,7 @@ apply plugin: 'com.android.library'
//apply plugin: 'me.tatarka.retrolambda'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion setup.compileSdk
android {
lintOptions {
@ -12,7 +12,7 @@ android {
defaultConfig {
minSdkVersion 14
targetSdkVersion rootProject.ext.targetSdkVersion
targetSdkVersion setup.targetSdk
versionCode 1
versionName "1.0"
}
@ -43,9 +43,9 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Google libraries
implementation "androidx.appcompat:appcompat:${androidXAppCompat}"
implementation "androidx.recyclerview:recyclerview:${androidXRecyclerView}"
implementation "com.google.android.material:material:${googleMaterial}"
implementation "androidx.appcompat:appcompat:${versions.appcompat}"
implementation "androidx.recyclerview:recyclerview:${versions.recyclerView}"
implementation "com.google.android.material:material:${versions.material}"
// other libraries
//implementation 'se.emilsjolander:stickylistheaders:2.7.0'

View File

@ -1,13 +1,14 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'io.fabric'
android {
signingConfigs {
}
compileSdkVersion rootProject.ext.compileSdkVersion
compileSdkVersion setup.compileSdk
defaultConfig {
applicationId 'pl.szczodrzynski.edziennik'
minSdkVersion setup.minSdk
@ -103,7 +104,7 @@ tasks.whenTaskAdded { task ->
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
annotationProcessor "androidx.room:room-compiler:${versions.room}"
kapt "androidx.room:room-compiler:${versions.room}"
debugImplementation "com.amitshekhar.android:debug-db:1.0.5"
implementation "android.arch.navigation:navigation-fragment-ktx:${versions.navigationFragment}"
@ -187,6 +188,8 @@ dependencies {
implementation "com.squareup.retrofit2:converter-gson:${versions.retrofit}"
implementation 'com.github.jetradarmobile:android-snowfall:1.2.0'
implementation "io.coil-kt:coil:0.9.2"
}
repositories {
mavenCentral()

View File

@ -28,7 +28,7 @@
-keepclassmembers class pl.szczodrzynski.edziennik.ui.widgets.WidgetConfig { public *; }
-keepnames class pl.szczodrzynski.edziennik.ui.widgets.timetable.WidgetTimetableProvider
-keepnames class pl.szczodrzynski.edziennik.ui.widgets.notifications.WidgetNotificationsProvider
-keepnames class pl.szczodrzynski.edziennik.widgets.luckynumber.WidgetLuckyNumber
-keepnames class pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider
-keep class .R
-keep class **.R$* {

View File

@ -80,6 +80,7 @@
<service android:name=".ui.widgets.timetable.WidgetTimetableService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<activity android:name=".ui.widgets.LessonDialogActivity"
android:label=""
android:configChanges="orientation|keyboardHidden"
android:excludeFromRecents="true"
android:noHistory="true"
@ -141,12 +142,8 @@
android:label="@string/app_name"
android:theme="@style/AppTheme" />
<activity android:name=".ui.modules.settings.SettingsLicenseActivity"
android:configChanges="orientation|keyboardHidden"
android:theme="@style/AppTheme" />
<activity android:name=".ui.modules.webpush.WebPushConfigActivity"
android:configChanges="orientation|keyboardHidden"
android:theme="@style/AppTheme.Dark" />
<activity android:name=".ui.modules.webpush.QrScannerActivity" />
<activity android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
android:configChanges="orientation|keyboardHidden"
@ -165,18 +162,9 @@
<action android:name="android.intent.action.USER_PRESENT" />
</intent-filter>
</receiver>
<receiver android:name=".receivers.BootReceiver">
<receiver android:name=".sync.UpdateDownloaderService$DownloadProgressReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<receiver android:name=".sync.FirebaseBroadcastReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</receiver>
<receiver android:name=".receivers.SzkolnyReceiver"
@ -193,16 +181,22 @@
____) | __/ | \ V /| | (_| __/\__ \
|_____/ \___|_| \_/ |_|\___\___||___/
-->
<service android:name=".sync.MyFirebaseMessagingService"
<!--<service android:name=".sync.MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<service android:name=".receivers.BootReceiver$NotificationActionService" />
<service android:name=".Notifier$GetDataRetryService" />
</service>-->
<service android:name=".data.api.ApiService" />
<service android:name=".data.firebase.MyFirebaseService"
android:exported="false">
<intent-filter android:priority="10000000">
<action android:name="com.google.firebase.MESSAGING_EVENT" />
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter>
</service>
<service android:name=".sync.UpdateDownloaderService" />
<!--
_____ _ _

View File

@ -1,4 +1,4 @@
<h3>Wersja 4.0-beta.3, 2020-01-10</h3>
<h3>Wersja 4.0-beta.6, 2020-01-28</h3>
<ul>
<li><b>Przebudowaliśmy cały moduł synchronizacji</b>, co oznacza większą stabilność aplikacji, szybkosć oraz poprawność pobieranych danych.</li>
<li><b><u>Wysyłanie wiadomości</u></b> - funkcja, na którą czekał każdy. Od teraz w Szkolnym można wysyłać oraz odpowiadać na wiadomości do nauczycieli &#x1F44F;</li>
@ -6,6 +6,7 @@
<li>Nowa <b>Strona główna</b> - ładniejszy wygląd oraz możliwość przestawiania kart na każdym profilu</li>
<li>Nowy <b>Plan lekcji</b> - z doskonałą obsługą lekcji przesuniętych oraz dwóch lekcji o tej samej godzinie</li>
<li>Nowe okienka informacji o wydarzeniach oraz lekcjach</li>
<li>Nowe, przyjemniejsze powiadomienia</li>
<li>Łatwiejsze dodawanie własnych wydarzeń</li>
<li>Dużo poprawek w widoku <b>Wiadomości</b> oraz <b>Ogłoszeń</b></li>
<li>Częściowa <b>Obsługa dziennika EduDziennik</b></li>
@ -14,7 +15,9 @@
<li>Librus: obsługa Zadań domowych bez posiadania Mobilnych dodatków (przez system Synergia)</li>
<li>Lepsze <b>przekazywanie powiadomień na komputer</b> oraz łatwiejsze parowanie</li>
<li>Poprawiliśmy synchronizację w tle na niektórych telefonach</li>
<li>Usunąłem denerwujący brak zaznaczenia w lewym menu</li>
<li>Znaczna ilość błędów z poprzednich wersji już nie występuje</li>
<li><strike>Występują natomiast nowe błędy, dlatego proszę o ich zgłaszanie :)</strike></li>
</ul>
<br>
<br>
@ -24,6 +27,7 @@ Staramy się usuwać takie przypadki, jednak na chwilę obecną mogą występowa
<ul>
<li>Wysyłanie wiadomości może czasami nie działać - proszę o zgłaszanie wszystkich błędów na naszym serwerze Discord</li>
<li>Terminarz - brak informacji o odwołanych lekcjach w dialogu</li>
<li>Cisza nocna w powiadomieniach jeszcze nie działa.</li>
</ul>
<br>
<br>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/
static toys AES_IV[16] = {
0x1e, 0xef, 0x4e, 0xc6, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
0x4a, 0x83, 0xba, 0x71, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -1,714 +0,0 @@
package pl.szczodrzynski.edziennik;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.content.pm.Signature;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Icon;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.provider.Settings;
import android.util.Base64;
import android.util.Log;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.work.Configuration;
import com.chuckerteam.chucker.api.ChuckerCollector;
import com.chuckerteam.chucker.api.ChuckerInterceptor;
import com.chuckerteam.chucker.api.RetentionManager;
import com.google.android.gms.security.ProviderInstaller;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import com.hypertrack.hyperlog.HyperLog;
import com.mikepenz.iconics.Iconics;
import com.mikepenz.iconics.IconicsColor;
import com.mikepenz.iconics.IconicsDrawable;
import com.mikepenz.iconics.IconicsSize;
import com.mikepenz.iconics.typeface.IIcon;
import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont;
import java.lang.reflect.Field;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import cat.ereza.customactivityoncrash.config.CaocConfig;
import im.wangchao.mhttp.MHttp;
import im.wangchao.mhttp.internal.cookie.PersistentCookieJar;
import im.wangchao.mhttp.internal.cookie.cache.SetCookieCache;
import im.wangchao.mhttp.internal.cookie.persistence.SharedPrefsCookiePersistor;
import me.leolin.shortcutbadger.ShortcutBadger;
import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient;
import okhttp3.TlsVersion;
import pl.szczodrzynski.edziennik.config.Config;
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing;
import pl.szczodrzynski.edziennik.data.api.task.EdziennikTask;
import pl.szczodrzynski.edziennik.data.db.AppDb;
import pl.szczodrzynski.edziennik.data.db.entity.DebugLog;
import pl.szczodrzynski.edziennik.data.db.entity.Profile;
import pl.szczodrzynski.edziennik.network.NetworkUtils;
import pl.szczodrzynski.edziennik.network.TLSSocketFactory;
import pl.szczodrzynski.edziennik.sync.SyncWorker;
import pl.szczodrzynski.edziennik.ui.modules.base.CrashActivity;
import pl.szczodrzynski.edziennik.utils.DebugLogFormat;
import pl.szczodrzynski.edziennik.utils.PermissionChecker;
import pl.szczodrzynski.edziennik.utils.Themes;
import pl.szczodrzynski.edziennik.utils.Utils;
import pl.szczodrzynski.edziennik.utils.models.AppConfig;
import static pl.szczodrzynski.edziennik.data.db.entity.LoginStore.LOGIN_TYPE_MOBIDZIENNIK;
public class App extends androidx.multidex.MultiDexApplication implements Configuration.Provider {
private static final String TAG = "App";
public static int profileId = -1;
private Context mContext;
@Override
public Configuration getWorkManagerConfiguration() {
return new Configuration.Builder()
.setMinimumLoggingLevel(Log.VERBOSE)
.build();
}
public static final int REQUEST_TIMEOUT = 10 * 1000;
// notifications
//public NotificationManager mNotificationManager;
//public final String NOTIFICATION_CHANNEL_ID_UPDATES = "4566";
//public String NOTIFICATION_CHANNEL_NAME_UPDATES;
public Notifier notifier;
public static final String APP_URL = "://edziennik.szczodrzynski.pl/app/";
public ShortcutManager shortcutManager;
public PermissionChecker permissionChecker;
public String signature = "";
public String deviceId = "";
public AppDb db;
public void debugLog(String text) {
if (!devMode)
return;
db.debugLogDao().add(new DebugLog(Utils.getCurrentTimeUsingCalendar()+": "+text));
}
public void debugLogAsync(String text) {
if (!devMode)
return;
AsyncTask.execute(() -> {
db.debugLogDao().add(new DebugLog(Utils.getCurrentTimeUsingCalendar()+": "+text));
});
}
// network & APIs
public NetworkUtils networkUtils;
public PersistentCookieJar cookieJar;
public OkHttpClient http;
public OkHttpClient httpLazy;
public SharedPreferences appSharedPrefs; // sharedPreferences for APPCONFIG + JOBS STORE
public AppConfig appConfig; // APPCONFIG: common for all profiles
//public AppProfile profile; // current profile
public SharedPreferences registerStore; // sharedPreferences for REGISTER
//public Register register; // REGISTER for current profile, read from registerStore
public Profile profile;
public Config config;
private static Config mConfig;
public static Config getConfig() {
return mConfig;
}
// other stuff
public Gson gson;
public String requestScheme = "https";
public boolean unreadBadgesAvailable = true;
public static boolean devMode = false;
public static final boolean UPDATES_ON_PLAY_STORE = true;
@RequiresApi(api = Build.VERSION_CODES.M)
public Icon getDesktopIconFromIconics(IIcon icon) {
final IconicsDrawable drawable = new IconicsDrawable(mContext, icon)
.color(IconicsColor.colorInt(Color.WHITE))
.size(IconicsSize.dp(48))
.padding(IconicsSize.dp(8))
.backgroundColor(IconicsColor.colorRes(R.color.colorPrimaryDark))
.roundedCorners(IconicsSize.dp(10));
//drawable.setStyle(Paint.Style.FILL);
final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return Icon.createWithBitmap(bitmap);
}
@Override
public void onCreate() {
super.onCreate();
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
CaocConfig.Builder.create()
.backgroundMode(CaocConfig.BACKGROUND_MODE_SHOW_CUSTOM) //default: CaocConfig.BACKGROUND_MODE_SHOW_CUSTOM
.enabled(true) //default: true
.showErrorDetails(true) //default: true
.showRestartButton(true) //default: true
.logErrorOnRestart(true) //default: true
.trackActivities(true) //default: false
.minTimeBetweenCrashesMs(2000) //default: 3000
.errorDrawable(R.drawable.ic_rip) //default: bug image
.restartActivity(MainActivity.class) //default: null (your app's launch activity)
.errorActivity(CrashActivity.class) //default: null (default error activity)
//.eventListener(new YourCustomEventListener()) //default: null
.apply();
mContext = this;
db = AppDb.getDatabase(this);
gson = new Gson();
networkUtils = new NetworkUtils(this);
config = new Config(db);
config.migrate(this);
mConfig = config;
Iconics.init(getApplicationContext());
Iconics.registerFont(SzkolnyFont.INSTANCE);
notifier = new Notifier(this);
permissionChecker = new PermissionChecker(mContext);
deviceId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
cookieJar = new PersistentCookieJar(new SetCookieCache(), new SharedPrefsCookiePersistor(this));
appSharedPrefs = getSharedPreferences(getString(R.string.preference_file_global), Context.MODE_PRIVATE);
loadConfig();
Signing.INSTANCE.getCert(this);
Themes.INSTANCE.setThemeInt(config.getUi().getTheme());
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
for (Signature signature: packageInfo.signatures) {
byte[] signatureBytes = signature.toByteArray();
MessageDigest md = MessageDigest.getInstance("SHA");
md.update(signatureBytes);
this.signature = Base64.encodeToString(md.digest(), Base64.NO_WRAP);
//Log.d(TAG, "Signature is "+this.signature);
}
}
catch (Exception e) {
e.printStackTrace();
}
if ("f054761fbdb6a238".equals(deviceId) || BuildConfig.DEBUG) {
devMode = true;
}
else if (config.getDevModePassword() != null) {
checkDevModePassword();
}
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder()
.cache(null)
.followRedirects(true)
.followSslRedirects(true)
.retryOnConnectionFailure(true)
.cookieJar(cookieJar)
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(40, TimeUnit.SECONDS);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
try {
try {
ProviderInstaller.installIfNeeded(this);
} catch (Exception e) {
Log.e("OkHttpTLSCompat", "Play Services not found or outdated");
X509TrustManager x509TrustManager = null;
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
for (TrustManager trustManager: trustManagerFactory.getTrustManagers()) {
if (trustManager instanceof X509TrustManager)
x509TrustManager = (X509TrustManager) trustManager;
}
SSLContext sc = SSLContext.getInstance("TLSv1.2");
sc.init(null, null, null);
httpBuilder.sslSocketFactory(new TLSSocketFactory(sc.getSocketFactory()), x509TrustManager);
ConnectionSpec cs = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_0)
.tlsVersions(TlsVersion.TLS_1_1)
.tlsVersions(TlsVersion.TLS_1_2)
.build();
List<ConnectionSpec> specs = new ArrayList<>();
specs.add(cs);
specs.add(ConnectionSpec.COMPATIBLE_TLS);
specs.add(ConnectionSpec.CLEARTEXT);
httpBuilder.connectionSpecs(specs);
}
} catch (Exception exc) {
Log.e("OkHttpTLSCompat", "Error while setting TLS 1.2", exc);
}
}
if (App.devMode || BuildConfig.DEBUG) {
HyperLog.initialize(this);
HyperLog.setLogLevel(Log.VERBOSE);
HyperLog.setLogFormat(new DebugLogFormat(this));
ChuckerCollector chuckerCollector = new ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR);
ChuckerInterceptor chuckerInterceptor = new ChuckerInterceptor(this, chuckerCollector);
httpBuilder.addInterceptor(chuckerInterceptor);
}
http = httpBuilder.build();
httpLazy = http.newBuilder().followRedirects(false).followSslRedirects(false).build();
MHttp.instance()
.customOkHttpClient(http);
//register = new Register(mContext);
//profileLoadById(appSharedPrefs.getInt("current_profile_id", 1));
if (config.getSync().getEnabled()) {
SyncWorker.Companion.scheduleNext(this, false);
}
else {
SyncWorker.Companion.cancelNext(this);
}
db.metadataDao().countUnseen().observeForever(count -> {
Log.d("MainActivity", "Overall unseen count changed");
assert count != null;
if (unreadBadgesAvailable) {
unreadBadgesAvailable = ShortcutBadger.applyCount(this, count);
}
});
//new IonCookieManager(mContext);
new Handler().post(() -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
shortcutManager = getSystemService(ShortcutManager.class);
ShortcutInfo shortcutTimetable = new ShortcutInfo.Builder(mContext, "item_timetable")
.setShortLabel(getString(R.string.shortcut_timetable)).setLongLabel(getString(R.string.shortcut_timetable))
.setIcon(Icon.createWithResource(this, R.mipmap.ic_shortcut_timetable))
//.setIcon(getDesktopIconFromIconics(CommunityMaterial.Icon2.cmd_timetable))
.setIntent(new Intent(Intent.ACTION_MAIN, null, this, MainActivity.class)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_TIMETABLE))
.build();
ShortcutInfo shortcutAgenda = new ShortcutInfo.Builder(mContext, "item_agenda")
.setShortLabel(getString(R.string.shortcut_agenda)).setLongLabel(getString(R.string.shortcut_agenda))
.setIcon(Icon.createWithResource(this, R.mipmap.ic_shortcut_agenda))
//.setIcon(getDesktopIconFromIconics(CommunityMaterial.Icon.cmd_calendar))
.setIntent(new Intent(Intent.ACTION_MAIN, null, this, MainActivity.class)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_AGENDA))
.build();
ShortcutInfo shortcutGrades = new ShortcutInfo.Builder(mContext, "item_grades")
.setShortLabel(getString(R.string.shortcut_grades)).setLongLabel(getString(R.string.shortcut_grades))
.setIcon(Icon.createWithResource(this, R.mipmap.ic_shortcut_grades))
//.setIcon(getDesktopIconFromIconics(CommunityMaterial.Icon2.cmd_numeric_5_box))
.setIntent(new Intent(Intent.ACTION_MAIN, null, this, MainActivity.class)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_GRADES))
.build();
ShortcutInfo shortcutHomework = new ShortcutInfo.Builder(mContext, "item_homeworks")
.setShortLabel(getString(R.string.shortcut_homework)).setLongLabel(getString(R.string.shortcut_homework))
.setIcon(Icon.createWithResource(this, R.mipmap.ic_shortcut_homework))
//.setIcon(getDesktopIconFromIconics(SzkolnyFont.Icon.szf_file_document_edit))
.setIntent(new Intent(Intent.ACTION_MAIN, null, this, MainActivity.class)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_HOMEWORK))
.build();
ShortcutInfo shortcutMessages = new ShortcutInfo.Builder(mContext, "item_messages")
.setShortLabel(getString(R.string.shortcut_messages)).setLongLabel(getString(R.string.shortcut_messages))
.setIcon(Icon.createWithResource(this, R.mipmap.ic_shortcut_messages))
//.setIcon(getDesktopIconFromIconics(CommunityMaterial.Icon.cmd_email))
.setIntent(new Intent(Intent.ACTION_MAIN, null, this, MainActivity.class)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_MESSAGES ))
.build();
shortcutManager.setDynamicShortcuts(Arrays.asList(shortcutTimetable, shortcutAgenda, shortcutGrades, shortcutHomework, shortcutMessages));
}
if (config.getAppInstalledTime() == 0) {
try {
config.setAppInstalledTime(getPackageManager().getPackageInfo(getPackageName(), 0).firstInstallTime);
config.setAppRateSnackbarTime(config.getAppInstalledTime() + 7 * 24 * 60 * 60 * 1000);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
/*Task<CapabilityInfo> capabilityInfoTask =
Wearable.getCapabilityClient(this)
.getCapability("edziennik_wear_app", CapabilityClient.FILTER_REACHABLE);
capabilityInfoTask.addOnCompleteListener((task) -> {
if (task.isSuccessful()) {
CapabilityInfo capabilityInfo = task.getResult();
assert capabilityInfo != null;
Set<Node> nodes;
nodes = capabilityInfo.getNodes();
Log.d(TAG, "Nodes "+nodes);
if (nodes.size() > 0) {
Wearable.getMessageClient(this).sendMessage(
nodes.toArray(new Node[]{})[0].getId(), "/ping", "Hello world".getBytes());
}
} else {
Log.d(TAG, "Capability request failed to return any results.");
}
});
Wearable.getDataClient(this).addListener(dataEventBuffer -> {
Log.d(TAG, "onDataChanged(): " + dataEventBuffer);
for (DataEvent event : dataEventBuffer) {
if (event.getType() == DataEvent.TYPE_CHANGED) {
String path = event.getDataItem().getUri().getPath();
Log.d(TAG, "Data "+path+ " :: "+Arrays.toString(event.getDataItem().getData()));
}
}
});*/
FirebaseApp pushMobidziennikApp = FirebaseApp.initializeApp(
this,
new FirebaseOptions.Builder()
.setApiKey("AIzaSyCi5LmsZ5BBCQnGtrdvWnp1bWLCNP8OWQE")
.setApplicationId("1:747285019373:android:f6341bf7b158621d")
.build(),
"Mobidziennik2"
);
FirebaseApp pushLibrusApp = FirebaseApp.initializeApp(
this,
new FirebaseOptions.Builder()
.setApiKey("AIzaSyDfTuEoYPKdv4aceEws1CO3n0-HvTndz-o")
.setApplicationId("1:513056078587:android:1e29083b760af544")
.build(),
"Librus"
);
FirebaseApp pushVulcanApp = FirebaseApp.initializeApp(
this,
new FirebaseOptions.Builder()
.setApiKey("AIzaSyDW8MUtanHy64_I0oCpY6cOxB3jrvJd_iA")
.setApplicationId("1:987828170337:android:ac97431a0a4578c3")
.build(),
"Vulcan"
);
if (config.getRunSync()) {
config.setRunSync(false);
EdziennikTask.Companion.sync().enqueue(this);
}
try {
final long startTime = System.currentTimeMillis();
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(instanceIdResult -> {
Log.d(TAG, "Token for App is " + instanceIdResult.getToken() + ", ID is " + instanceIdResult.getId()+". Time is "+(System.currentTimeMillis() - startTime));
config.getSync().setTokenApp(instanceIdResult.getToken());
});
/*FirebaseInstanceId.getInstance(pushMobidziennikApp).getInstanceId().addOnSuccessListener(instanceIdResult -> {
Log.d(TAG, "Token for Mobidziennik is " + instanceIdResult.getToken() + ", ID is " + instanceIdResult.getId());
appConfig.fcmTokens.put(LOGIN_TYPE_MOBIDZIENNIK, new Pair<>(instanceIdResult.getToken(), new ArrayList<>()));
});
FirebaseInstanceId.getInstance(pushLibrusApp).getInstanceId().addOnSuccessListener(instanceIdResult -> {
Log.d(TAG, "Token for Librus is " + instanceIdResult.getToken() + ", ID is " + instanceIdResult.getId());
appConfig.fcmTokens.put(LOGIN_TYPE_LIBRUS, new Pair<>(instanceIdResult.getToken(), new ArrayList<>()));
});
FirebaseInstanceId.getInstance(pushVulcanApp).getInstanceId().addOnSuccessListener(instanceIdResult -> {
Log.d(TAG, "Token for Vulcan is " + instanceIdResult.getToken() + ", ID is " + instanceIdResult.getId());
Pair<String, List<Integer>> pair = appConfig.fcmTokens.get(LOGIN_TYPE_VULCAN);
if (pair == null || pair.first == null || !pair.first.equals(instanceIdResult.getToken())) {
appConfig.fcmTokens.put(LOGIN_TYPE_VULCAN, new Pair<>(instanceIdResult.getToken(), new ArrayList<>()));
}
});*/
FirebaseMessaging.getInstance().subscribeToTopic(getPackageName());
}
catch (IllegalStateException e) {
e.printStackTrace();
}
});
}
public void loadConfig()
{
appConfig = new AppConfig(this);
if (appSharedPrefs.contains("config")) {
// remove old-format config, save the new one and empty the incorrectly-nulled config
appConfig = gson.fromJson(appSharedPrefs.getString("config", ""), AppConfig.class);
appSharedPrefs.edit().remove("config").apply();
saveConfig();
appConfig = new AppConfig(this);
}
if (appSharedPrefs.contains("profiles")) {
SharedPreferences.Editor appSharedPrefsEditor = appSharedPrefs.edit();
/*List<Integer> appProfileIds = gson.fromJson(appSharedPrefs.getString("profiles", ""), new TypeToken<List<Integer>>(){}.getType());
for (int id: appProfileIds) {
AppProfile appProfile = gson.fromJson(appSharedPrefs.getString("profile"+id, ""), AppProfile.class);
if (appProfile != null) {
appConfig.profiles.add(appProfile);
}
appSharedPrefsEditor.remove("profile"+id);
}*/
appSharedPrefsEditor.remove("profiles");
appSharedPrefsEditor.apply();
//profilesSave();
}
Map<String,?> keys = appSharedPrefs.getAll();
for (Map.Entry<String,?> entry : keys.entrySet()) {
if (entry.getKey().startsWith("app.appConfig.")) {
String fieldName = entry.getKey().replace("app.appConfig.", "");
try {
Field field = AppConfig.class.getField(fieldName);
Object object;
try {
object = gson.fromJson(entry.getValue().toString(), field.getGenericType());
} catch (JsonSyntaxException e) {
Log.d(TAG, "For field "+fieldName);
e.printStackTrace();
object = entry.getValue().toString();
}
if (object != null)
field.set(appConfig, object);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
Log.w(TAG, "Should remove app.appConfig."+fieldName);
//appSharedPrefs.edit().remove("app.appConfig."+fieldName).apply(); TODO migration
}
}
}
/*if (appConfig.lastAppVersion > BuildConfig.VERSION_CODE) {
BootReceiver br = new BootReceiver();
Intent i = new Intent();
//i.putExtra("UserChecked", true);
br.onReceive(getContext(), i);
Toast.makeText(mContext, R.string.warning_older_version_running, Toast.LENGTH_LONG).show();
//Toast.makeText(mContext, "Zaktualizuj aplikację.", Toast.LENGTH_LONG).show();
//System.exit(0);
}*/
if (appConfig == null) {
appConfig = new AppConfig(this);
}
}
public void saveConfig()
{
try {
appConfig.savePending = false;
SharedPreferences.Editor appSharedPrefsEditor = appSharedPrefs.edit();
JsonObject appConfigJson = gson.toJsonTree(appConfig).getAsJsonObject();
for (Map.Entry<String, JsonElement> entry : appConfigJson.entrySet()) {
String jsonObj;
jsonObj = entry.getValue().toString();
/*if (entry.getValue().isJsonObject()) {
jsonObj = entry.getValue().getAsJsonObject().toString();
}
else if (entry.getValue().isJsonArray()) {
jsonObj = entry.getValue().getAsJsonArray().toString();
}
else {
jsonObj = entry.getValue().toString();
}*/
appSharedPrefsEditor.putString("app.appConfig." + entry.getKey(), jsonObj);
}
appSharedPrefsEditor.apply();
}
catch (ConcurrentModificationException e) {
e.printStackTrace();
}
//appSharedPrefs.edit().putString("config", gson.toJson(appConfig)).apply();
}
public void saveConfig(String ... fieldNames)
{
appConfig.savePending = false;
SharedPreferences.Editor appSharedPrefsEditor = appSharedPrefs.edit();
for (String fieldName: fieldNames) {
try {
Object object = AppConfig.class.getField(fieldName).get(appConfig);
appSharedPrefsEditor.putString("app.appConfig."+fieldName, gson.toJson(object));
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (ConcurrentModificationException e) {
e.printStackTrace();
}
}
appSharedPrefsEditor.apply();
//appSharedPrefs.edit().putString("config", gson.toJson(appConfig)).apply();
}
public void profileSave() {
AsyncTask.execute(() -> {
db.profileDao().add(profile);
});
}
public void profileSaveAsync() {
AsyncTask.execute(() -> {
db.profileDao().add(profile);
});
}
public void profileSaveAsync(Profile profile) {
AsyncTask.execute(() -> {
db.profileDao().add(profile);
});
}
public void profileLoadById(int id) {
profileLoadById(id, false);
}
public void profileLoadById(int id, boolean loadedLast) {
//Log.d(TAG, "Loading ID "+id);
/*if (profile == null) {
profile = profileNew();
AppDb.profileId = profile.id;
appSharedPrefs.edit().putInt("current_profile_id", profile.id).apply();
return;
}*/
if (profile == null || profile.getId() != id) {
profile = db.profileDao().getByIdNow(id);
/*if (profile == null) {
profileLoadById(id);
return;
}*/
if (profile != null) {
MainActivity.Companion.setUseOldMessages(profile.getLoginStoreType() == LOGIN_TYPE_MOBIDZIENNIK && appConfig.mobidziennikOldMessages == 1);
profileId = profile.getId();
appSharedPrefs.edit().putInt("current_profile_id", profile.getId()).apply();
config.setProfile(profileId);
}
else if (!loadedLast) {
profileLoadById(profileLastId(), true);
}
else {
profileId = -1;
}
}
}
/*public void profileRemove(int id)
{
Profile profile = db.profileDao().getFullByIdNow(id);
if (profile.id == profile.loginStoreId) {
// this profile is the owner of the login store
// we need to check if any other profile is using it
List<Integer> transferProfileIds = db.profileDao().getIdsByLoginStoreIdNow(profile.loginStoreId);
if (transferProfileIds.size() == 1) {
// this login store is free of users, remove it along with the profile
db.loginStoreDao().remove(profile.loginStoreId);
// the current store is removed, we are ready to remove the profile
}
else if (transferProfileIds.size() > 1) {
transferProfileIds.remove(transferProfileIds.indexOf(profile.id));
// someone is using the store
// we need to transfer it to the firstProfileId
db.loginStoreDao().changeId(profile.loginStoreId, transferProfileIds.get(0));
db.profileDao().changeStoreId(profile.loginStoreId, transferProfileIds.get(0));
// the current store is removed, we are ready to remove the profile
}
}
// else, the profile uses a store that it doesn't own
// leave the store and go on with removing
Log.d(TAG, "Before removal: "+db.profileDao().getAllNow().toString());
db.profileDao().remove(profile.id);
Log.d(TAG, "After removal: "+db.profileDao().getAllNow().toString());
*//*int newId = 1;
if (appConfig.profiles.size() > 0) {
newId = appConfig.profiles.get(appConfig.profiles.size() - 1).id;
}
Log.d(TAG, "New ID: "+newId);
//Toast.makeText(mContext, "selected new id "+newId, Toast.LENGTH_SHORT).show();
profileLoadById(newId);*//*
}*/
public int profileFirstId() {
Integer id = db.profileDao().getFirstId();
return id == null ? 1 : id;
}
public int profileLastId() {
Integer id = db.profileDao().getLastId();
return id == null ? 1 : id;
}
public Context getContext()
{
return mContext;
}
public void checkDevModePassword() {
try {
devMode = Utils.AESCrypt.decrypt("nWFVxY65Pa8/aRrT7EylNAencmOD+IxUY2Gg/beiIWY=", config.getDevModePassword()).equals("ok here you go it's enabled now")
|| BuildConfig.DEBUG;
} catch (Exception e) {
e.printStackTrace();
devMode = false;
}
}
}

View File

@ -4,20 +4,93 @@
package pl.szczodrzynski.edziennik
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Build
import android.provider.Settings
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDexApplication
import androidx.work.Configuration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import cat.ereza.customactivityoncrash.config.CaocConfig
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.chuckerteam.chucker.api.RetentionManager
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import com.google.firebase.iid.FirebaseInstanceId
import com.google.firebase.messaging.FirebaseMessaging
import com.google.gson.Gson
import com.hypertrack.hyperlog.HyperLog
import com.mikepenz.iconics.Iconics
import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont
import im.wangchao.mhttp.MHttp
import im.wangchao.mhttp.internal.cookie.PersistentCookieJar
import im.wangchao.mhttp.internal.cookie.cache.SetCookieCache
import im.wangchao.mhttp.internal.cookie.persistence.SharedPrefsCookiePersistor
import kotlinx.coroutines.*
import me.leolin.shortcutbadger.ShortcutBadger
import okhttp3.OkHttpClient
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.config.Config
import pl.szczodrzynski.edziennik.data.api.events.ProfileListEmptyEvent
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing
import pl.szczodrzynski.edziennik.data.db.AppDb
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.network.NetworkUtils
import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.modules.base.CrashActivity
import pl.szczodrzynski.edziennik.utils.*
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
class Szkolny : /*MultiDexApplication(),*/ Configuration.Provider, CoroutineScope {
class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
companion object {
@Volatile
lateinit var db: AppDb
val config: Config by lazy { Config(db) }
var profile: Profile by mutableLazy { Profile(0, 0, 0, "") }
val profileId
get() = profile.id
var devMode = false
}
//lateinit var db: AppDb
//val config by lazy { Config(db); // TODO migrate }
val notifications by lazy { Notifications() }
inner class Notifications {
val syncId = 1
val syncKey = "pl.szczodrzynski.edziennik.SYNC"
val syncChannelName: String by lazy { getString(R.string.notification_channel_get_data_name) }
val syncChannelDesc: String by lazy { getString(R.string.notification_channel_get_data_desc) }
val dataId = 50
val dataKey = "pl.szczodrzynski.edziennik.DATA"
val dataChannelName: String by lazy { getString(R.string.notification_channel_notifications_name) }
val dataChannelDesc: String by lazy { getString(R.string.notification_channel_notifications_desc) }
val dataQuietId = 60
val dataQuietKey = "pl.szczodrzynski.edziennik.DATA_QUIET"
val dataQuietChannelName: String by lazy { getString(R.string.notification_channel_notifications_quiet_name) }
val dataQuietChannelDesc: String by lazy { getString(R.string.notification_channel_notifications_quiet_desc) }
val updatesId = 100
val updatesKey = "pl.szczodrzynski.edziennik.UPDATES"
val updatesChannelName: String by lazy { getString(R.string.notification_channel_updates_name) }
val updatesChannelDesc: String by lazy { getString(R.string.notification_channel_updates_desc) }
}
val db
get() = App.db
val config
get() = App.config
val profile
get() = App.profile
val profileId
get() = App.profileId
private val job = Job()
override val coroutineContext: CoroutineContext
@ -26,11 +99,10 @@ class Szkolny : /*MultiDexApplication(),*/ Configuration.Provider, CoroutineScop
.setMinimumLoggingLevel(Log.VERBOSE)
.build()
/*val preferences by lazy { getSharedPreferences(getString(R.string.preference_file), Context.MODE_PRIVATE) }
val notifier by lazy { Notifier(this) }
val preferences by lazy { getSharedPreferences(getString(R.string.preference_file), Context.MODE_PRIVATE) }
val permissionChecker by lazy { PermissionChecker(this) }
lateinit var profile: ProfileFull
val networkUtils by lazy { NetworkUtils(this) }
val gson by lazy { Gson() }
/* _ _ _______ _______ _____
| | | |__ __|__ __| __ \
@ -48,13 +120,13 @@ class Szkolny : /*MultiDexApplication(),*/ Configuration.Provider, CoroutineScop
.connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
builder.installHttpsSupport()
builder.installHttpsSupport(this)
if (devMode || BuildConfig.DEBUG) {
HyperLog.initialize(this)
HyperLog.setLogLevel(Log.VERBOSE)
HyperLog.setLogFormat(DebugLogFormat(this))
val chuckerCollector = ChuckerCollector(this, true, Period.ONE_HOUR)
val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR)
val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector)
builder.addInterceptor(chuckerInterceptor)
}
@ -77,22 +149,7 @@ class Szkolny : /*MultiDexApplication(),*/ Configuration.Provider, CoroutineScop
|_____/|_|\__, |_| |_|\__,_|\__|\__,_|_| \___|
__/ |
|__*/
private val deviceId: String by lazy { Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) ?: "" }
private val signature: String by lazy {
var str = ""
try {
val packageInfo: PackageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
for (signature in packageInfo.signatures) {
val signatureBytes = signature.toByteArray()
val md = MessageDigest.getInstance("SHA")
md.update(signatureBytes)
str = Base64.encodeToString(md.digest(), Base64.DEFAULT)
}
} catch (e: Exception) {
e.printStackTrace()
}
str
}
val deviceId: String by lazy { Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) ?: "" }
private var unreadBadgesAvailable = true
/* _____ _
@ -118,193 +175,214 @@ class Szkolny : /*MultiDexApplication(),*/ Configuration.Provider, CoroutineScop
.apply()
Iconics.init(applicationContext)
Iconics.registerFont(SzkolnyFont)
db = AppDb.getDatabase(this)
App.db = AppDb(this)
Themes.themeInt = config.ui.theme
MHttp.instance().customOkHttpClient(http)
if (!profileLoadById(config.lastProfileId)) {
db.profileDao().firstId?.let { profileLoadById(it) }
}
devMode = "f054761fbdb6a238" == deviceId || BuildConfig.DEBUG
if (config.devModePassword != null)
checkDevModePassword()
Signing.getCert(this)
launch { async(Dispatchers.Default) {
if (config.sync.enabled) {
scheduleNext(this@App, false)
} else {
cancelNext(this@App)
launch {
withContext(Dispatchers.Default) {
config.migrate(this@App)
if (config.devModePassword != null)
checkDevModePassword()
if (config.sync.enabled)
SyncWorker.scheduleNext(this@App, false)
else
SyncWorker.cancelNext(this@App)
if (config.sync.notifyAboutUpdates)
UpdateWorker.scheduleNext(this@App, false)
else
UpdateWorker.cancelNext(this@App)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
val shortcutManager = getSystemService(ShortcutManager::class.java)
val shortcutTimetable = ShortcutInfo.Builder(this@App, "item_timetable")
.setShortLabel(getString(R.string.shortcut_timetable)).setLongLabel(getString(R.string.shortcut_timetable))
.setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_timetable))
.setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_TIMETABLE))
.build()
val shortcutAgenda = ShortcutInfo.Builder(this@App, "item_agenda")
.setShortLabel(getString(R.string.shortcut_agenda)).setLongLabel(getString(R.string.shortcut_agenda))
.setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_agenda))
.setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_AGENDA))
.build()
val shortcutGrades = ShortcutInfo.Builder(this@App, "item_grades")
.setShortLabel(getString(R.string.shortcut_grades)).setLongLabel(getString(R.string.shortcut_grades))
.setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_grades))
.setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_GRADES))
.build()
val shortcutHomework = ShortcutInfo.Builder(this@App, "item_homeworks")
.setShortLabel(getString(R.string.shortcut_homework)).setLongLabel(getString(R.string.shortcut_homework))
.setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_homework))
.setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_HOMEWORK))
.build()
val shortcutMessages = ShortcutInfo.Builder(this@App, "item_messages")
.setShortLabel(getString(R.string.shortcut_messages)).setLongLabel(getString(R.string.shortcut_messages))
.setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_messages))
.setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_MESSAGES))
.build()
shortcutManager.dynamicShortcuts = listOf(
shortcutTimetable,
shortcutAgenda,
shortcutGrades,
shortcutHomework,
shortcutMessages
)
} // shortcuts - end
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(
NotificationChannel(notifications.syncKey, notifications.syncChannelName, NotificationManager.IMPORTANCE_MIN).apply {
description = notifications.syncChannelDesc
})
notificationManager.createNotificationChannel(
NotificationChannel(notifications.dataKey, notifications.dataChannelName, NotificationManager.IMPORTANCE_HIGH).apply {
description = notifications.dataChannelDesc
enableLights(true)
lightColor = 0xff2196f3.toInt()
})
notificationManager.createNotificationChannel(
NotificationChannel(notifications.dataQuietKey, notifications.dataQuietChannelName, NotificationManager.IMPORTANCE_LOW).apply {
description = notifications.dataQuietChannelDesc
setSound(null, null)
enableVibration(false)
})
notificationManager.createNotificationChannel(
NotificationChannel(notifications.updatesKey, notifications.updatesChannelName, NotificationManager.IMPORTANCE_DEFAULT).apply {
description = notifications.updatesChannelDesc
})
}
if (config.appInstalledTime == 0L)
try {
config.appInstalledTime = packageManager.getPackageInfo(packageName, 0).firstInstallTime
config.appRateSnackbarTime = config.appInstalledTime + 7 * DAY * MS
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
val pushMobidziennikApp = FirebaseApp.initializeApp(
this@App,
FirebaseOptions.Builder()
.setApiKey("AIzaSyCi5LmsZ5BBCQnGtrdvWnp1bWLCNP8OWQE")
.setApplicationId("1:747285019373:android:f6341bf7b158621d")
.build(),
"Mobidziennik2"
)
val pushLibrusApp = FirebaseApp.initializeApp(
this@App,
FirebaseOptions.Builder()
.setApiKey("AIzaSyDfTuEoYPKdv4aceEws1CO3n0-HvTndz-o")
.setApplicationId("1:513056078587:android:1e29083b760af544")
.build(),
"Librus"
)
val pushVulcanApp = FirebaseApp.initializeApp(
this@App,
FirebaseOptions.Builder()
.setApiKey("AIzaSyDW8MUtanHy64_I0oCpY6cOxB3jrvJd_iA")
.setApplicationId("1:987828170337:android:ac97431a0a4578c3")
.build(),
"Vulcan"
)
try {
FirebaseInstanceId.getInstance().instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
config.sync.tokenApp = token
}
FirebaseInstanceId.getInstance(pushMobidziennikApp).instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
if (token != config.sync.tokenMobidziennik) {
config.sync.tokenMobidziennik = token
config.sync.tokenMobidziennikList = listOf()
}
}
FirebaseInstanceId.getInstance(pushLibrusApp).instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
if (token != config.sync.tokenLibrus) {
config.sync.tokenLibrus = token
config.sync.tokenLibrusList = listOf()
}
}
FirebaseInstanceId.getInstance(pushVulcanApp).instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
if (token != config.sync.tokenVulcan) {
config.sync.tokenVulcan = token
config.sync.tokenVulcanList = listOf()
}
}
FirebaseMessaging.getInstance().subscribeToTopic(packageName)
} catch (e: IllegalStateException) {
e.printStackTrace()
}
}
db.metadataDao().countUnseen().observeForever { count: Int ->
if (unreadBadgesAvailable)
unreadBadgesAvailable = ShortcutBadger.applyCount(this@App, count)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
val shortcutManager = getSystemService(ShortcutManager::class.java)
val shortcutTimetable = ShortcutInfo.Builder(this@App, "item_timetable")
.setShortLabel(getString(R.string.shortcut_timetable)).setLongLabel(getString(R.string.shortcut_timetable))
.setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_timetable))
.setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_TIMETABLE))
.build()
val shortcutAgenda = ShortcutInfo.Builder(this@App, "item_agenda")
.setShortLabel(getString(R.string.shortcut_agenda)).setLongLabel(getString(R.string.shortcut_agenda))
.setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_agenda))
.setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_AGENDA))
.build()
val shortcutGrades = ShortcutInfo.Builder(this@App, "item_grades")
.setShortLabel(getString(R.string.shortcut_grades)).setLongLabel(getString(R.string.shortcut_grades))
.setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_grades))
.setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_GRADES))
.build()
val shortcutHomework = ShortcutInfo.Builder(this@App, "item_homeworks")
.setShortLabel(getString(R.string.shortcut_homework)).setLongLabel(getString(R.string.shortcut_homework))
.setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_homework))
.setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_HOMEWORK))
.build()
val shortcutMessages = ShortcutInfo.Builder(this@App, "item_messages")
.setShortLabel(getString(R.string.shortcut_messages)).setLongLabel(getString(R.string.shortcut_messages))
.setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_messages))
.setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java)
.putExtra("fragmentId", MainActivity.DRAWER_ITEM_MESSAGES))
.build()
shortcutManager.dynamicShortcuts = listOf(
shortcutTimetable,
shortcutAgenda,
shortcutGrades,
shortcutHomework,
shortcutMessages
)
} // shortcuts - end
if (config.appInstalledTime == 0L)
try {
config.appInstalledTime = packageManager.getPackageInfo(packageName, 0).firstInstallTime
config.appRateSnackbarTime = config.appInstalledTime + 7*DAY*MS
} catch (e: NameNotFoundException) {
e.printStackTrace()
}
val pushMobidziennikApp = FirebaseApp.initializeApp(
this@App,
FirebaseOptions.Builder()
.setApiKey("AIzaSyCi5LmsZ5BBCQnGtrdvWnp1bWLCNP8OWQE")
.setApplicationId("1:747285019373:android:f6341bf7b158621d")
.build(),
"Mobidziennik2"
)
val pushLibrusApp = FirebaseApp.initializeApp(
this@App,
FirebaseOptions.Builder()
.setApiKey("AIzaSyDfTuEoYPKdv4aceEws1CO3n0-HvTndz-o")
.setApplicationId("1:513056078587:android:1e29083b760af544")
.build(),
"Librus"
)
val pushVulcanApp = FirebaseApp.initializeApp(
this@App,
FirebaseOptions.Builder()
.setApiKey("AIzaSyDW8MUtanHy64_I0oCpY6cOxB3jrvJd_iA")
.setApplicationId("1:987828170337:android:ac97431a0a4578c3")
.build(),
"Vulcan"
)
try {
FirebaseInstanceId.getInstance().instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
config.sync.tokenApp = token
}
FirebaseInstanceId.getInstance(pushMobidziennikApp).instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
if (token != config.sync.tokenMobidziennik) {
config.sync.tokenMobidziennik = token
config.sync.tokenMobidziennikList = listOf()
}
}
FirebaseInstanceId.getInstance(pushLibrusApp).instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
if (token != config.sync.tokenLibrus) {
config.sync.tokenLibrus = token
config.sync.tokenLibrusList = listOf()
}
}
FirebaseInstanceId.getInstance(pushVulcanApp).instanceId.addOnSuccessListener { instanceIdResult ->
val token = instanceIdResult.token
if (token != config.sync.tokenVulcan) {
config.sync.tokenVulcan = token
config.sync.tokenVulcanList = listOf()
}
}
FirebaseMessaging.getInstance().subscribeToTopic(packageName)
} catch (e: IllegalStateException) {
e.printStackTrace()
}
}}
}
private fun profileLoad(profileId: Int) {
db.profileDao().getFullByIdNow(profileId)?.also {
profile = it
} ?: run {
if (!::profile.isInitialized) {
profile = ProfileFull(-1, "", "", -1)
}
}
}
fun profileLoad(profileId: Int, onSuccess: (profile: ProfileFull) -> Unit) {
private fun profileLoadById(profileId: Int): Boolean {
db.profileDao().getByIdNow(profileId)?.also {
App.profile = it
App.config.lastProfileId = it.id
return true
}
return false
}
fun profileLoad(profileId: Int, onSuccess: (profile: Profile) -> Unit) {
launch {
val deferred = async(Dispatchers.Default) {
profileLoad(profileId)
val success = withContext(Dispatchers.Default) {
profileLoadById(profileId)
}
deferred.await()
onSuccess(profile)
if (success)
onSuccess(profile)
else
profileLoadLast(onSuccess)
}
}
private fun OkHttpClient.Builder.installHttpsSupport() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
try {
try {
ProviderInstaller.installIfNeeded(this@App)
} catch (e: Exception) {
Log.e("OkHttpTLSCompat", "Play Services not found or outdated")
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(null as KeyStore?)
val x509TrustManager = trustManagerFactory.trustManagers.singleOrNull { it is X509TrustManager } as X509TrustManager?
?: return
val sc = SSLContext.getInstance("TLSv1.2")
sc.init(null, null, null)
sslSocketFactory(TLSSocketFactory(sc.socketFactory), x509TrustManager)
val cs: ConnectionSpec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_0)
.tlsVersions(TlsVersion.TLS_1_1)
.tlsVersions(TlsVersion.TLS_1_2)
.build()
val specs: MutableList<ConnectionSpec> = ArrayList()
specs.add(cs)
specs.add(ConnectionSpec.COMPATIBLE_TLS)
specs.add(ConnectionSpec.CLEARTEXT)
connectionSpecs(specs)
}
} catch (exc: Exception) {
Log.e("OkHttpTLSCompat", "Error while setting TLS 1.2", exc)
fun profileLoadLast(onSuccess: (profile: Profile) -> Unit) {
launch {
val success = withContext(Dispatchers.Default) {
profileLoadById(db.profileDao().lastId ?: return@withContext false)
}
if (!success) {
EventBus.getDefault().post(ProfileListEmptyEvent())
}
}
}
fun profileSave() = profileSave(profile)
fun profileSave(profile: Profile) {
launch(Dispatchers.Default) {
App.db.profileDao().add(profile)
}
}
@ -315,5 +393,5 @@ class Szkolny : /*MultiDexApplication(),*/ Configuration.Provider, CoroutineScop
e.printStackTrace()
false
}
}*/
}
}

View File

@ -3,6 +3,7 @@ package pl.szczodrzynski.edziennik
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.content.res.Resources
@ -24,6 +25,7 @@ import android.util.Base64.encodeToString
import android.view.View
import android.widget.CheckBox
import android.widget.CompoundButton
import android.widget.RadioButton
import android.widget.TextView
import androidx.annotation.*
import androidx.core.app.ActivityCompat
@ -34,6 +36,7 @@ import androidx.core.util.forEach
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import com.google.android.gms.security.ProviderInstaller
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
@ -41,23 +44,31 @@ import im.wangchao.mhttp.Response
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import okhttp3.ConnectionSpec
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.TlsVersion
import okio.Buffer
import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.entity.Team
import pl.szczodrzynski.edziennik.network.TLSSocketFactory
import pl.szczodrzynski.edziennik.utils.models.Time
import java.io.PrintWriter
import java.io.StringWriter
import java.math.BigInteger
import java.nio.charset.Charset
import java.security.KeyStore
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.*
import java.util.zip.CRC32
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import kotlin.Pair
@ -642,6 +653,34 @@ fun Bundle(vararg properties: Pair<String, Any?>): Bundle {
}
}
}
fun Intent(action: String? = null, vararg properties: Pair<String, Any?>): Intent {
return Intent(action).putExtras(Bundle(*properties))
}
fun Intent(packageContext: Context, cls: Class<*>, vararg properties: Pair<String, Any?>): Intent {
return Intent(packageContext, cls).putExtras(Bundle(*properties))
}
fun Bundle.toJsonObject(): JsonObject {
val json = JsonObject()
keySet()?.forEach { key ->
get(key)?.let {
when (it) {
is String -> json.addProperty(key, it)
is Char -> json.addProperty(key, it)
is Int -> json.addProperty(key, it)
is Long -> json.addProperty(key, it)
is Float -> json.addProperty(key, it)
is Short -> json.addProperty(key, it)
is Double -> json.addProperty(key, it)
is Boolean -> json.addProperty(key, it)
is Bundle -> json.add(key, it.toJsonObject())
else -> json.addProperty(key, it.toString())
}
}
}
return json
}
fun Intent.toJsonObject() = extras?.toJsonObject()
fun JsonArray?.isNullOrEmpty(): Boolean = (this?.size() ?: 0) == 0
fun JsonArray.isEmpty(): Boolean = this.size() == 0
@ -934,6 +973,8 @@ fun Context.getNotificationTitle(type: Int): String {
Notification.TYPE_NEW_EVENT -> R.string.notification_type_new_event
Notification.TYPE_NEW_HOMEWORK -> R.string.notification_type_new_homework
Notification.TYPE_NEW_SHARED_EVENT -> R.string.notification_type_new_shared_event
Notification.TYPE_NEW_SHARED_HOMEWORK -> R.string.notification_type_new_shared_homework
Notification.TYPE_REMOVED_SHARED_EVENT -> R.string.notification_type_removed_shared_event
Notification.TYPE_NEW_MESSAGE -> R.string.notification_type_new_message
Notification.TYPE_NEW_NOTICE -> R.string.notification_type_notice
Notification.TYPE_NEW_ATTENDANCE -> R.string.notification_type_attendance
@ -950,3 +991,48 @@ fun Context.getNotificationTitle(type: Int): String {
fun Cursor?.getString(columnName: String) = this?.getStringOrNull(getColumnIndex(columnName))
fun Cursor?.getInt(columnName: String) = this?.getIntOrNull(getColumnIndex(columnName))
fun Cursor?.getLong(columnName: String) = this?.getLongOrNull(getColumnIndex(columnName))
fun OkHttpClient.Builder.installHttpsSupport(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
try {
try {
ProviderInstaller.installIfNeeded(context)
} catch (e: Exception) {
Log.e("OkHttpTLSCompat", "Play Services not found or outdated")
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(null as KeyStore?)
val x509TrustManager = trustManagerFactory.trustManagers.singleOrNull { it is X509TrustManager } as X509TrustManager?
?: return
val sc = SSLContext.getInstance("TLSv1.2")
sc.init(null, null, null)
sslSocketFactory(TLSSocketFactory(sc.socketFactory), x509TrustManager)
val cs: ConnectionSpec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_0)
.tlsVersions(TlsVersion.TLS_1_1)
.tlsVersions(TlsVersion.TLS_1_2)
.build()
val specs: MutableList<ConnectionSpec> = ArrayList()
specs.add(cs)
specs.add(ConnectionSpec.COMPATIBLE_TLS)
specs.add(ConnectionSpec.CLEARTEXT)
connectionSpecs(specs)
}
} catch (exc: Exception) {
Log.e("OkHttpTLSCompat", "Error while setting TLS 1.2", exc)
}
}
}
fun CharSequence.containsAll(list: List<CharSequence>, ignoreCase: Boolean = false): Boolean {
for (i in list) {
if (!contains(i, ignoreCase))
return false
}
return true
}
fun RadioButton.setOnSelectedListener(listener: (buttonView: CompoundButton) -> Unit)
= setOnCheckedChangeListener { buttonView, isChecked -> if (isChecked) listener(buttonView) }

View File

@ -1,6 +1,5 @@
package pl.szczodrzynski.edziennik
import android.app.Activity
import android.app.ActivityManager
import android.content.BroadcastReceiver
import android.content.Context
@ -9,7 +8,6 @@ import android.content.IntentFilter
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
import android.os.Environment
@ -36,18 +34,22 @@ import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IProfile
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.droidsonroids.gif.GifDrawable
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing
import pl.szczodrzynski.edziennik.data.api.task.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Metadata.*
import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding
import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent
import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog
import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog
@ -93,9 +95,10 @@ import pl.szczodrzynski.navlib.drawer.items.withAppTitle
import java.io.File
import java.io.IOException
import java.util.*
import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToInt
class MainActivity : AppCompatActivity() {
class MainActivity : AppCompatActivity(), CoroutineScope {
companion object {
var useOldMessages = false
@ -108,6 +111,7 @@ class MainActivity : AppCompatActivity() {
const val DRAWER_PROFILE_SYNC_ALL = 201
const val DRAWER_PROFILE_EXPORT_DATA = 202
const val DRAWER_PROFILE_MANAGE = 203
const val DRAWER_PROFILE_MARK_ALL_AS_READ = 204
const val DRAWER_ITEM_HOME = 1
const val DRAWER_ITEM_TIMETABLE = 11
const val DRAWER_ITEM_AGENDA = 12
@ -208,6 +212,10 @@ class MainActivity : AppCompatActivity() {
.withDescription(R.string.drawer_manage_profiles_desc)
.isInProfileList(false)
list += NavTarget(DRAWER_PROFILE_MARK_ALL_AS_READ, R.string.menu_mark_everything_as_read, null)
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
.isInProfileList(true)
list += NavTarget(DRAWER_PROFILE_SYNC_ALL, R.string.menu_sync_all, null)
.withIcon(CommunityMaterial.Icon.cmd_download_outline)
.isInProfileList(true)
@ -226,6 +234,10 @@ class MainActivity : AppCompatActivity() {
}
}
private var job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
val b: ActivitySzkolnyBinding by lazy { ActivitySzkolnyBinding.inflate(layoutInflater) }
val navView: NavView by lazy { b.navView }
val drawer: NavDrawer by lazy { navView.drawer }
@ -257,12 +269,21 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
d(TAG, "Activity created")
setTheme(Themes.appTheme)
app.config.ui.language?.let {
setLanguage(it)
}
if (App.profileId == 0) {
onProfileListEmptyEvent(ProfileListEmptyEvent())
return
}
d(TAG, "Profile is valid, inflating views")
setContentView(b.root)
Log.d(TAG, Signing.appPassword)
@ -332,8 +353,7 @@ class MainActivity : AppCompatActivity() {
setAccountHeaderBackground(app.config.ui.headerBackground)
drawerProfileListEmptyListener = {
app.config.loginFinished = false
profileListEmptyListener()
onProfileListEmptyEvent(ProfileListEmptyEvent())
}
drawerItemSelectedListener = { id, position, drawerItem ->
loadTarget(id)
@ -362,30 +382,19 @@ class MainActivity : AppCompatActivity() {
navTarget = navTargetList[0]
var profileListEmpty = drawer.profileListEmpty
if (savedInstanceState != null) {
intent?.putExtras(savedInstanceState)
savedInstanceState.clear()
}
if (!profileListEmpty) {
handleIntent(intent?.extras)
}
app.db.profileDao().all.observe(this, Observer { profiles ->
drawer.setProfileList(profiles.filter { it.id >= 0 }.toMutableList())
if (profileListEmpty) {
profileListEmpty = false
handleIntent(intent?.extras)
}
else if (app.profile != null) {
drawer.currentProfile = app.profile.id
}
drawer.currentProfile = App.profileId
})
// if null, getAllFull will load a profile and update drawerItems
if (app.profile != null)
setDrawerItems()
setDrawerItems()
handleIntent(intent?.extras)
app.db.metadataDao().unreadCounts.observe(this, Observer { unreadCounters ->
unreadCounters.map {
@ -405,6 +414,7 @@ class MainActivity : AppCompatActivity() {
isStoragePermissionGranted()
SyncWorker.scheduleNext(app)
UpdateWorker.scheduleNext(app)
// APP BACKGROUND
if (app.config.ui.appBackground != null) {
@ -509,17 +519,25 @@ class MainActivity : AppCompatActivity() {
}
}
var profileListEmptyListener = {
startActivityForResult(Intent(this, LoginActivity::class.java), REQUEST_LOGIN_ACTIVITY)
}
private var profileSettingClickListener = { id: Int, view: View? ->
when (id) {
DRAWER_PROFILE_ADD_NEW -> {
profileListEmptyListener()
startActivityForResult(Intent(this, LoginActivity::class.java), REQUEST_LOGIN_ACTIVITY)
}
DRAWER_PROFILE_SYNC_ALL -> {
EdziennikTask.sync().enqueue(this)
}
DRAWER_PROFILE_MARK_ALL_AS_READ -> { launch {
withContext(Dispatchers.Default) {
app.db.profileDao().allNow.forEach { profile ->
if (profile.loginStoreType != LoginStore.LOGIN_TYPE_LIBRUS)
app.db.metadataDao().setAllSeenExceptMessagesAndAnnouncements(profile.id, true)
else
app.db.metadataDao().setAllSeenExceptMessages(profile.id, true)
}
}
Toast.makeText(this@MainActivity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show()
}}
else -> {
loadTarget(id)
}
@ -564,6 +582,13 @@ class MainActivity : AppCompatActivity() {
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onProfileListEmptyEvent(event: ProfileListEmptyEvent) {
d(TAG, "Profile list is empty. Launch LoginActivity.")
app.config.loginFinished = false
startActivity(Intent(this, LoginActivity::class.java))
finish()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onApiTaskProgressEvent(event: ApiTaskProgressEvent) {
if (event.profileId == App.profileId) {
navView.toolbar.apply {
@ -579,6 +604,7 @@ class MainActivity : AppCompatActivity() {
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onApiTaskFinishedEvent(event: ApiTaskFinishedEvent) {
EventBus.getDefault().removeStickyEvent(event)
if (event.profileId == App.profileId) {
navView.toolbar.apply {
subtitleFormat = R.string.toolbar_subtitle
@ -589,6 +615,7 @@ class MainActivity : AppCompatActivity() {
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onApiTaskAllFinishedEvent(event: ApiTaskAllFinishedEvent) {
EventBus.getDefault().removeStickyEvent(event)
swipeRefreshLayout.isRefreshing = false
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
@ -604,7 +631,8 @@ class MainActivity : AppCompatActivity() {
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onAppManagerDetectedEvent(event: AppManagerDetectedEvent) {
if (app.appConfig.dontShowAppManagerDialog)
EventBus.getDefault().removeStickyEvent(event)
if (app.config.sync.dontShowAppManagerDialog)
return
MaterialAlertDialogBuilder(this)
.setTitle(R.string.app_manager_dialog_title)
@ -626,8 +654,7 @@ class MainActivity : AppCompatActivity() {
}
}
.setNeutralButton(R.string.dont_ask_again) { dialog, which ->
app.appConfig.dontShowAppManagerDialog = true
app.saveConfig("dontShowAppManagerDialog")
app.config.sync.dontShowAppManagerDialog = true
}
.setCancelable(false)
.show()
@ -669,26 +696,46 @@ class MainActivity : AppCompatActivity() {
}
d(TAG, "}")
var intentProfileId = -1
var intentTargetId = -1
if (extras?.containsKey("action") == true) {
val handled = when (extras.getString("action")) {
"serverMessage" -> {
ServerMessageDialog(
this,
extras.getString("serverMessageTitle") ?: getString(R.string.app_name),
extras.getString("serverMessageText") ?: ""
)
true
}
"feedbackMessage" -> {
intentTargetId = TARGET_FEEDBACK
false
}
else -> false
}
if (handled)
return
}
if (extras?.containsKey("reloadProfileId") == true) {
val reloadProfileId = extras.getInt("reloadProfileId", -1)
extras.remove("reloadProfileId")
if (reloadProfileId == -1 || (app.profile != null && app.profile.id == reloadProfileId)) {
if (reloadProfileId == -1 || app.profile.id == reloadProfileId) {
reloadTarget()
return
}
}
var intentProfileId = -1
var intentTargetId = -1
if (extras?.containsKey("profileId") == true) {
if (extras?.getInt("profileId", -1) != -1) {
intentProfileId = extras.getInt("profileId", -1)
extras.remove("profileId")
extras?.remove("profileId")
}
if (extras?.containsKey("fragmentId") == true) {
if (extras?.getInt("fragmentId", -1) != -1) {
intentTargetId = extras.getInt("fragmentId", -1)
extras.remove("fragmentId")
extras?.remove("fragmentId")
}
/*if (intentTargetId == -1 && navController.currentDestination?.id == R.id.loadingFragment) {
@ -702,9 +749,9 @@ class MainActivity : AppCompatActivity() {
}
when {
app.profile == null || app.profile.id == -1 -> {
app.profile.id == 0 -> {
if (intentProfileId == -1)
intentProfileId = app.appSharedPrefs.getInt("current_profile_id", 1)
intentProfileId = app.config.lastProfileId
loadProfile(intentProfileId, intentTargetId, extras)
}
intentProfileId != -1 -> {
@ -743,7 +790,16 @@ class MainActivity : AppCompatActivity() {
startActivity(intent)
}
override fun onStart() {
d(TAG, "Activity started")
super.onStart()
}
override fun onStop() {
d(TAG, "Activity stopped")
super.onStop()
}
override fun onResume() {
d(TAG, "Activity resumed")
val filter = IntentFilter()
filter.addAction(Intent.ACTION_MAIN)
registerReceiver(intentReceiver, filter)
@ -751,10 +807,15 @@ class MainActivity : AppCompatActivity() {
super.onResume()
}
override fun onPause() {
d(TAG, "Activity paused")
unregisterReceiver(intentReceiver)
EventBus.getDefault().unregister(this)
super.onPause()
}
override fun onDestroy() {
d(TAG, "Activity destroyed")
super.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
@ -768,15 +829,10 @@ class MainActivity : AppCompatActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_LOGIN_ACTIVITY) {
if (resultCode == Activity.RESULT_CANCELED && false) {
if (!app.config.loginFinished)
finish()
}
else {
if (!app.config.loginFinished)
finish()
else {
handleIntent(data?.extras)
}
handleIntent(data?.extras)
}
}
}
@ -797,34 +853,23 @@ class MainActivity : AppCompatActivity() {
fun loadProfile(id: Int) = loadProfile(id, navTargetId)
fun loadProfile(id: Int, arguments: Bundle?) = loadProfile(id, navTargetId, arguments)
fun loadProfile(id: Int, drawerSelection: Int, arguments: Bundle? = null) {
//d("NavDebug", "loadProfile(id = $id, drawerSelection = $drawerSelection)")
if (app.profile != null && App.profileId == id) {
if (App.profileId == id) {
drawer.currentProfile = app.profile.id
loadTarget(drawerSelection, arguments)
return
}
AsyncTask.execute {
app.profileLoadById(id)
app.profileLoad(id) {
MessagesFragment.pageSelection = -1
MessagesListFragment.tapPositions = intArrayOf(RecyclerView.NO_POSITION, RecyclerView.NO_POSITION)
MessagesListFragment.topPositions = intArrayOf(RecyclerView.NO_POSITION, RecyclerView.NO_POSITION)
MessagesListFragment.bottomPositions = intArrayOf(RecyclerView.NO_POSITION, RecyclerView.NO_POSITION)
this.runOnUiThread {
if (app.profile == null) {
if (app.config.loginFinished) {
// this shouldn't run
profileListEmptyListener()
}
} else {
setDrawerItems()
// the drawer profile is updated automatically when the drawer item is clicked
// update it manually when switching profiles from other source
//if (drawer.currentProfile != app.profile.id)
drawer.currentProfile = app.profile.id
loadTarget(drawerSelection, arguments)
}
}
setDrawerItems()
// the drawer profile is updated automatically when the drawer item is clicked
// update it manually when switching profiles from other source
//if (drawer.currentProfile != app.profile.id)
drawer.currentProfile = app.profileId
loadTarget(drawerSelection, arguments)
}
}
fun loadTarget(id: Int, arguments: Bundle? = null) {
@ -969,7 +1014,7 @@ class MainActivity : AppCompatActivity() {
* that something has changed in the bottom sheet.
*/
fun gainAttention() {
if (app.config.ui.bottomSheetOpened || true)
if (app.config.ui.bottomSheetOpened)
return
b.navView.postDelayed({
navView.gainAttentionOnBottomBar()
@ -1018,12 +1063,11 @@ class MainActivity : AppCompatActivity() {
}
fun setDrawerItems() {
d("NavDebug", "setDrawerItems() app.profile = ${app.profile ?: "null"}")
d("NavDebug", "setDrawerItems() app.profile = ${app.profile}")
val drawerItems = arrayListOf<IDrawerItem<*>>()
val drawerProfiles = arrayListOf<ProfileSettingDrawerItem>()
val supportedFragments = if (app.profile == null) arrayListOf<Int>()
else app.profile.supportedFragments
val supportedFragments = app.profile.supportedFragments
targetPopToHomeList.clear()
@ -1087,7 +1131,7 @@ class MainActivity : AppCompatActivity() {
private var targetHomeId: Int = -1
override fun onBackPressed() {
if (!b.navView.onBackPressed()) {
if (App.getConfig().ui.openDrawerOnBackPressed) {
if (App.config.ui.openDrawerOnBackPressed) {
b.navView.drawer.toggle()
} else {
navigateUp()

View File

@ -1,350 +0,0 @@
package pl.szczodrzynski.edziennik;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import pl.szczodrzynski.edziennik.receivers.BootReceiver;
import pl.szczodrzynski.edziennik.utils.models.Date;
import pl.szczodrzynski.edziennik.utils.models.Time;
import static androidx.core.app.NotificationCompat.PRIORITY_DEFAULT;
import static androidx.core.app.NotificationCompat.PRIORITY_MAX;
public class Notifier {
private static final String TAG = "Notifier";
public static final int ID_GET_DATA = 1337000;
public static final int ID_GET_DATA_ERROR = 1337001;
private static String CHANNEL_GET_DATA_NAME;
private static String CHANNEL_GET_DATA_DESC;
private static final String GROUP_KEY_GET_DATA = "pl.szczodrzynski.edziennik.GET_DATA";
public static final int ID_NOTIFICATIONS = 1337002;
public static String CHANNEL_NOTIFICATIONS_NAME;
public static String CHANNEL_NOTIFICATIONS_DESC;
public static final String GROUP_KEY_NOTIFICATIONS = "pl.szczodrzynski.edziennik.NOTIFICATIONS";
public static final int ID_NOTIFICATIONS_QUIET = 1337002;
public static String CHANNEL_NOTIFICATIONS_QUIET_NAME;
public static String CHANNEL_NOTIFICATIONS_QUIET_DESC;
public static final String GROUP_KEY_NOTIFICATIONS_QUIET = "pl.szczodrzynski.edziennik.NOTIFICATIONS_QUIET";
private static final int ID_UPDATES = 1337003;
private static String CHANNEL_UPDATES_NAME;
private static String CHANNEL_UPDATES_DESC;
private static final String GROUP_KEY_UPDATES = "pl.szczodrzynski.edziennik.UPDATES";
private App app;
public NotificationManager notificationManager;
private NotificationCompat.Builder getDataNotificationBuilder;
public int notificationColor;
Notifier(App _app) {
this.app = _app;
CHANNEL_GET_DATA_NAME = app.getString(R.string.notification_channel_get_data_name);
CHANNEL_GET_DATA_DESC = app.getString(R.string.notification_channel_get_data_desc);
CHANNEL_NOTIFICATIONS_NAME = app.getString(R.string.notification_channel_notifications_name);
CHANNEL_NOTIFICATIONS_DESC = app.getString(R.string.notification_channel_notifications_desc);
CHANNEL_NOTIFICATIONS_QUIET_NAME = app.getString(R.string.notification_channel_notifications_quiet_name);
CHANNEL_NOTIFICATIONS_QUIET_DESC = app.getString(R.string.notification_channel_notifications_quiet_desc);
CHANNEL_UPDATES_NAME = app.getString(R.string.notification_channel_updates_name);
CHANNEL_UPDATES_DESC = app.getString(R.string.notification_channel_updates_desc);
notificationColor = ContextCompat.getColor(app.getContext(), R.color.colorPrimary);
notificationManager = (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channelGetData = new NotificationChannel(GROUP_KEY_GET_DATA, CHANNEL_GET_DATA_NAME, NotificationManager.IMPORTANCE_MIN);
channelGetData.setDescription(CHANNEL_GET_DATA_DESC);
notificationManager.createNotificationChannel(channelGetData);
NotificationChannel channelNotifications = new NotificationChannel(GROUP_KEY_NOTIFICATIONS, CHANNEL_NOTIFICATIONS_NAME, NotificationManager.IMPORTANCE_HIGH);
channelNotifications.setDescription(CHANNEL_NOTIFICATIONS_DESC);
channelNotifications.enableLights(true);
channelNotifications.setLightColor(notificationColor);
notificationManager.createNotificationChannel(channelNotifications);
NotificationChannel channelNotificationsQuiet = new NotificationChannel(GROUP_KEY_NOTIFICATIONS_QUIET, CHANNEL_NOTIFICATIONS_QUIET_NAME, NotificationManager.IMPORTANCE_DEFAULT);
channelNotificationsQuiet.setDescription(CHANNEL_NOTIFICATIONS_QUIET_DESC);
channelNotificationsQuiet.setSound(null, null);
channelNotificationsQuiet.enableVibration(false);
notificationManager.createNotificationChannel(channelNotificationsQuiet);
NotificationChannel channelUpdates = new NotificationChannel(GROUP_KEY_UPDATES, CHANNEL_UPDATES_NAME, NotificationManager.IMPORTANCE_HIGH);
channelUpdates.setDescription(CHANNEL_UPDATES_DESC);
notificationManager.createNotificationChannel(channelUpdates);
}
}
public boolean shouldBeQuiet() {
long now = Time.getNow().getInMillis();
long start = app.config.getSync().getQuietHoursStart();
long end = app.config.getSync().getQuietHoursEnd();
if (start > end) {
end += 1000 * 60 * 60 * 24;
//Log.d(TAG, "Night passing");
}
if (start > now) {
now += 1000 * 60 * 60 * 24;
//Log.d(TAG, "Now is smaller");
}
//Log.d(TAG, "Start is "+start+", now is "+now+", end is "+end);
return start > 0 && now >= start && now <= end;
}
public int getNotificationDefaults() {
return (shouldBeQuiet() ? 0 : Notification.DEFAULT_ALL);
}
public String getNotificationGroup() {
return shouldBeQuiet() ? GROUP_KEY_NOTIFICATIONS_QUIET : GROUP_KEY_NOTIFICATIONS;
}
public int getNotificationPriority() {
return shouldBeQuiet() ? PRIORITY_DEFAULT : PRIORITY_MAX;
}
/* _____ _ _____ _
| __ \ | | / ____| | |
| | | | __ _| |_ __ _ | | __ ___| |_
| | | |/ _` | __/ _` | | | |_ |/ _ \ __|
| |__| | (_| | || (_| | | |__| | __/ |_
|_____/ \__,_|\__\__,_| \_____|\___|\_*/
public Notification notificationGetDataShow(int maxProgress) {
/*Intent notificationIntent = new Intent(app.getContext(), SyncService.class);
notificationIntent.setAction(ACTION_CANCEL);
PendingIntent pendingIntent = PendingIntent.getService(app.getContext(), 0,
notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT);*/
getDataNotificationBuilder = new NotificationCompat.Builder(app, GROUP_KEY_GET_DATA)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setColor(notificationColor)
.setContentTitle(app.getString(R.string.notification_get_data_title))
.setContentText(app.getString(R.string.notification_get_data_text))
//.addAction(R.drawable.ic_notification, app.getString(R.string.notification_get_data_cancel), pendingIntent)
//.setGroup(GROUP_KEY_GET_DATA)
.setOngoing(true)
.setProgress(maxProgress, 0, false)
.setTicker(app.getString(R.string.notification_get_data_summary))
.setPriority(NotificationCompat.PRIORITY_LOW);
return getDataNotificationBuilder.build();
}
public Notification notificationGetDataProgress(int progress, int maxProgress) {
getDataNotificationBuilder.setProgress(maxProgress, progress, false);
return getDataNotificationBuilder.build();
}
public Notification notificationGetDataAction(int stringResId) {
getDataNotificationBuilder.setContentTitle(app.getString(R.string.sync_action_format, app.getString(stringResId)));
return getDataNotificationBuilder.build();
}
public Notification notificationGetDataProfile(String profileName) {
getDataNotificationBuilder.setContentText(profileName);
return getDataNotificationBuilder.build();
}
public Notification notificationGetDataError(String profileName, String error, int failedProfileId) {
Intent notificationIntent = new Intent(app.getContext(), Notifier.GetDataRetryService.class);
notificationIntent.putExtra("failedProfileId", failedProfileId);
PendingIntent pendingIntent = PendingIntent.getService(app.getContext(), 0,
notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT);
getDataNotificationBuilder.mActions.clear();
/*try {
//Use reflection clean up old actions
Field f = getDataNotificationBuilder.getClass().getDeclaredField("mActions");
f.setAccessible(true);
f.set(getDataNotificationBuilder, new ArrayList<NotificationCompat.Action>());
} catch (Exception e) {
e.printStackTrace();
}*/
getDataNotificationBuilder.setProgress(0, 0, false)
.setTicker(app.getString(R.string.notification_get_data_error_summary))
.setSmallIcon(android.R.drawable.stat_sys_warning)
.addAction(R.drawable.ic_notification, app.getString(R.string.notification_get_data_once_again), pendingIntent)
.setContentTitle(app.getString(R.string.notification_get_data_error_title, profileName))
.setContentText(error)
.setStyle(new NotificationCompat.BigTextStyle().bigText(error))
.setOngoing(false);
return getDataNotificationBuilder.build();
}
public void notificationPost(int id, Notification notification) {
notificationManager.notify(id, notification);
}
public void notificationCancel(int id) {
notificationManager.cancel(id);
}
//public void notificationGetDataHide() {
// notificationManager.cancel(ID_GET_DATA);
// }
public static class GetDataRetryService extends IntentService {
private static final String TAG = "Notifier/GetDataRetry";
public GetDataRetryService() {
super(Notifier.GetDataRetryService.class.getSimpleName());
}
@Override
protected void onHandleIntent(Intent intent) {
}
}
/* _ _ _ _ __ _ _ _
| \ | | | | (_)/ _(_) | | (_)
| \| | ___ | |_ _| |_ _ ___ __ _| |_ _ ___ _ __
| . ` |/ _ \| __| | _| |/ __/ _` | __| |/ _ \| '_ \
| |\ | (_) | |_| | | | | (_| (_| | |_| | (_) | | | |
|_| \_|\___/ \__|_|_| |_|\___\__,_|\__|_|\___/|_| |*/
public void add(pl.szczodrzynski.edziennik.utils.models.Notification notification) {
app.appConfig.notifications.add(notification);
}
public void postAll() {
Collections.sort(app.appConfig.notifications, (o1, o2) -> (o2.addedDate - o1.addedDate > 0) ? 1 : (o2.addedDate - o1.addedDate < 0) ? -1 : 0);
if (app.appConfig.notifications.size() > 40) {
app.appConfig.notifications.subList(40, app.appConfig.notifications.size() - 1).clear();
}
int unreadCount = 0;
List<pl.szczodrzynski.edziennik.utils.models.Notification> notificationList = new ArrayList<>();
for (pl.szczodrzynski.edziennik.utils.models.Notification notification: app.appConfig.notifications) {
if (!notification.notified) {
notification.seen = false;
notification.notified = true;
unreadCount++;
if (notificationList.size() < 10) {
notificationList.add(notification);
}
}
else {
notification.seen = true;
}
}
for (pl.szczodrzynski.edziennik.utils.models.Notification notification: notificationList) {
Intent intent = new Intent(app, MainActivity.class);
notification.fillIntent(intent);
PendingIntent pendingIntent = PendingIntent.getActivity(app, notification.id, intent, 0);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(app, getNotificationGroup())
// title, text, type, date
.setContentTitle(notification.title)
.setContentText(notification.text)
.setSubText(pl.szczodrzynski.edziennik.utils.models.Notification.stringType(app, notification.type))
.setWhen(notification.addedDate)
.setTicker(app.getString(R.string.notification_ticker_format, pl.szczodrzynski.edziennik.utils.models.Notification.stringType(app, notification.type)))
// icon, color, lights, priority
.setSmallIcon(R.drawable.ic_notification)
.setColor(notificationColor)
.setLights(0xFF00FFFF, 2000, 2000)
.setPriority(getNotificationPriority())
// channel, group, style
.setChannelId(getNotificationGroup())
.setGroup(getNotificationGroup())
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setStyle(new NotificationCompat.BigTextStyle().bigText(notification.text))
// intent, auto cancel
.setContentIntent(pendingIntent)
.setAutoCancel(true);
if (!shouldBeQuiet()) {
notificationBuilder.setDefaults(getNotificationDefaults());
}
notificationManager.notify(notification.id, notificationBuilder.build());
}
if (notificationList.size() > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Intent intent = new Intent(app, MainActivity.class);
intent.setAction("android.intent.action.MAIN");
intent.putExtra("fragmentId", MainActivity.DRAWER_ITEM_NOTIFICATIONS);
PendingIntent pendingIntent = PendingIntent.getActivity(app, ID_NOTIFICATIONS,
intent, 0);
NotificationCompat.Builder groupBuilder =
new NotificationCompat.Builder(app, getNotificationGroup())
.setSmallIcon(R.drawable.ic_notification)
.setColor(notificationColor)
.setContentTitle(app.getString(R.string.notification_new_notification_title_format, unreadCount))
.setGroupSummary(true)
.setAutoCancel(true)
.setChannelId(getNotificationGroup())
.setGroup(getNotificationGroup())
.setLights(0xFF00FFFF, 2000, 2000)
.setPriority(getNotificationPriority())
.setContentIntent(pendingIntent)
.setStyle(new NotificationCompat.BigTextStyle());
if (!shouldBeQuiet()) {
groupBuilder.setDefaults(getNotificationDefaults());
}
notificationManager.notify(ID_NOTIFICATIONS, groupBuilder.build());
}
}
/* _ _ _ _
| | | | | | | |
| | | |_ __ __| | __ _| |_ ___ ___
| | | | '_ \ / _` |/ _` | __/ _ \/ __|
| |__| | |_) | (_| | (_| | || __/\__ \
\____/| .__/ \__,_|\__,_|\__\___||___/
| |
|*/
public void notificationUpdatesShow(String updateVersion, String updateUrl, String updateFilename, boolean updateDirect) {
if (!app.config.getSync().getNotifyAboutUpdates())
return;
Intent notificationIntent = new Intent(app.getContext(), BootReceiver.NotificationActionService.class)
.putExtra("update_version", updateVersion)
.putExtra("update_url", updateUrl)
.putExtra("update_filename", updateFilename)
.putExtra("update_direct", updateDirect);
PendingIntent pendingIntent = PendingIntent.getService(app.getContext(), 0,
notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(app, GROUP_KEY_UPDATES)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setColor(notificationColor)
.setContentTitle(app.getString(R.string.notification_updates_title))
.setContentText(app.getString(R.string.notification_updates_text, updateVersion))
.setLights(0xFF00FFFF, 2000, 2000)
.setContentIntent(pendingIntent)
.setTicker(app.getString(R.string.notification_updates_summary))
.setPriority(PRIORITY_MAX)
.setAutoCancel(true);
if (!shouldBeQuiet()) {
notificationBuilder.setDefaults(getNotificationDefaults());
}
notificationManager.notify(ID_UPDATES, notificationBuilder.build());
}
public void notificationUpdatesHide() {
if (!app.config.getSync().getNotifyAboutUpdates())
return;
notificationManager.cancel(ID_UPDATES);
}
public void dump() {
for (pl.szczodrzynski.edziennik.utils.models.Notification notification: app.appConfig.notifications) {
Log.d(TAG, "Profile"+notification.profileId+" Notification from "+ Date.fromMillis(notification.addedDate).getFormattedString()+" "+ Time.fromMillis(notification.addedDate).getStringHMS()+" - "+notification.text);
}
}
}

View File

@ -16,12 +16,13 @@ import pl.szczodrzynski.edziennik.config.utils.ConfigMigration
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.config.utils.toHashMap
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.db.AppDb
import kotlin.coroutines.CoroutineContext
class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
companion object {
const val DATA_VERSION = 2
const val DATA_VERSION = 10
}
private val job = Job()
@ -45,6 +46,20 @@ class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
get() { mHash = mHash ?: values.get("hash", ""); return mHash ?: "" }
set(value) { set("hash", value); mHash = value }
private var mLastProfileId: Int? = null
var lastProfileId: Int
get() { mLastProfileId = mLastProfileId ?: values.get("lastProfileId", 0); return mLastProfileId ?: 0 }
set(value) { set("lastProfileId", value); mLastProfileId = value }
private var mUpdatesChannel: String? = null
var updatesChannel: String
get() { mUpdatesChannel = mUpdatesChannel ?: values.get("updatesChannel", "release"); return mUpdatesChannel ?: "release" }
set(value) { set("updatesChannel", value); mUpdatesChannel = value }
private var mUpdate: Update? = null
var update: Update?
get() { mUpdate = mUpdate ?: values.get("update", null as Update?); return mUpdate ?: null as Update? }
set(value) { set("update", value); mUpdate = value }
private var mAppVersion: Int? = null
var appVersion: Int
get() { mAppVersion = mAppVersion ?: values.get("appVersion", BuildConfig.VERSION_CODE); return mAppVersion ?: BuildConfig.VERSION_CODE }

View File

@ -17,6 +17,6 @@ class ConfigGrades(private val config: Config) {
private var mOrderBy: Int? = null
var orderBy: Int
get() { mOrderBy = mOrderBy ?: config.values.get("gradesOrderBy", 0); return mOrderBy ?: 0 }
get() { mOrderBy = mOrderBy ?: config.values.get("gradesOrderBy", 0); return mOrderBy ?: ORDER_BY_DATE_DESC }
set(value) { config.set("gradesOrderBy", value); mOrderBy = value }
}

View File

@ -9,6 +9,11 @@ import pl.szczodrzynski.edziennik.config.utils.getIntList
import pl.szczodrzynski.edziennik.config.utils.set
class ConfigSync(private val config: Config) {
private var mDontShowAppManagerDialog: Boolean? = null
var dontShowAppManagerDialog: Boolean
get() { mDontShowAppManagerDialog = mDontShowAppManagerDialog ?: config.values.get("dontShowAppManagerDialog", false); return mDontShowAppManagerDialog ?: false }
set(value) { config.set("dontShowAppManagerDialog", value); mDontShowAppManagerDialog = value }
private var mSyncEnabled: Boolean? = null
var enabled: Boolean
get() { mSyncEnabled = mSyncEnabled ?: config.values.get("syncEnabled", true); return mSyncEnabled ?: true }

View File

@ -45,11 +45,6 @@ class ConfigUI(private val config: Config) {
get() { mOpenDrawerOnBackPressed = mOpenDrawerOnBackPressed ?: config.values.get("openDrawerOnBackPressed", false); return mOpenDrawerOnBackPressed ?: false }
set(value) { config.set("openDrawerOnBackPressed", value); mOpenDrawerOnBackPressed = value }
private var mAgendaViewType: Int? = null
var agendaViewType: Int
get() { mAgendaViewType = mAgendaViewType ?: config.values.get("agendaViewType", 0); return mAgendaViewType ?: 0 }
set(value) { config.set("agendaViewType", value); mAgendaViewType = value }
private var mHomeCards: List<HomeCardModel>? = null
var homeCards: List<HomeCardModel>
get() { mHomeCards = mHomeCards ?: config.values.get("homeCards", listOf(), HomeCardModel::class.java); return mHomeCards ?: listOf() }

View File

@ -9,6 +9,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.config.db.ConfigEntry
import pl.szczodrzynski.edziennik.config.utils.ProfileConfigMigration
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.config.utils.toHashMap
@ -27,6 +28,7 @@ class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List<ConfigEn
val values: HashMap<String, String?> = hashMapOf()
val grades by lazy { ProfileConfigGrades(this) }
val ui by lazy { ProfileConfigUI(this) }
/*
val sync by lazy { ConfigSync(this) }
val timetable by lazy { ConfigTimetable(this) }
@ -44,8 +46,8 @@ class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List<ConfigEn
init {
rawEntries.toHashMap(profileId, values)
/*if (dataVersion < DATA_VERSION)
ProfileConfigMigration(this)*/
if (dataVersion < DATA_VERSION)
ProfileConfigMigration(this)
}
override fun set(key: String, value: String?) {

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-19.
*/
package pl.szczodrzynski.edziennik.config
import pl.szczodrzynski.edziennik.config.utils.get
import pl.szczodrzynski.edziennik.config.utils.set
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT
class ProfileConfigUI(private val config: ProfileConfig) {
private var mAgendaViewType: Int? = null
var agendaViewType: Int
get() { mAgendaViewType = mAgendaViewType ?: config.values.get("agendaViewType", 0); return mAgendaViewType ?: AGENDA_DEFAULT }
set(value) { config.set("agendaViewType", value); mAgendaViewType = value }
}

View File

@ -22,4 +22,7 @@ interface ConfigDao {
@Query("SELECT * FROM config WHERE profileId = :profileId")
fun getAllNow(profileId: Int): List<ConfigEntry>
@Query("DELETE FROM config WHERE profileId = :profileId")
fun clear(profileId: Int)
}

View File

@ -0,0 +1,81 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-19.
*/
package pl.szczodrzynski.edziennik.config.utils
import android.content.SharedPreferences
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.config.Config
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_VULCAN
import pl.szczodrzynski.edziennik.utils.models.Time
class AppConfigMigrationV3(p: SharedPreferences, config: Config) {
init { config.apply {
val s = "app.appConfig"
if (dataVersion < 1) {
ui.theme = p.getString("$s.appTheme", null)?.toIntOrNull() ?: 1
sync.enabled = p.getString("$s.registerSyncEnabled", null)?.toBoolean() ?: true
sync.interval = p.getString("$s.registerSyncInterval", null)?.toIntOrNull() ?: 3600
val oldButtons = p.getString("$s.miniDrawerButtonIds", null)?.let { str ->
str.replace("[\\[\\]]*".toRegex(), "")
.split(",\\s?".toRegex())
.mapNotNull { it.toIntOrNull() }
}
ui.miniMenuButtons = oldButtons ?: listOf(
MainActivity.DRAWER_ITEM_HOME,
MainActivity.DRAWER_ITEM_TIMETABLE,
MainActivity.DRAWER_ITEM_AGENDA,
MainActivity.DRAWER_ITEM_GRADES,
MainActivity.DRAWER_ITEM_MESSAGES,
MainActivity.DRAWER_ITEM_HOMEWORK,
MainActivity.DRAWER_ITEM_SETTINGS
)
dataVersion = 1
}
if (dataVersion < 2) {
devModePassword = p.getString("$s.devModePassword", null).fix()
sync.tokenApp = p.getString("$s.fcmToken", null).fix()
timetable.bellSyncMultiplier = p.getString("$s.bellSyncMultiplier", null)?.toIntOrNull() ?: 0
sync.quietHoursStart = p.getString("$s.quietHoursStart", null)?.toLongOrNull() ?: 0
appRateSnackbarTime = p.getString("$s.appRateSnackbarTime", null)?.toLongOrNull() ?: 0
sync.quietHoursEnd = p.getString("$s.quietHoursEnd", null)?.toLongOrNull() ?: 0
timetable.countInSeconds = p.getString("$s.countInSeconds", null)?.toBoolean() ?: false
ui.headerBackground = p.getString("$s.headerBackground", null).fix()
ui.appBackground = p.getString("$s.appBackground", null).fix()
ui.language = p.getString("$s.language", null).fix()
appVersion = p.getString("$s.lastAppVersion", null)?.toIntOrNull() ?: BuildConfig.VERSION_CODE
appInstalledTime = p.getString("$s.appInstalledTime", null)?.toLongOrNull() ?: 0
grades.orderBy = p.getString("$s.gradesOrderBy", null)?.toIntOrNull() ?: 0
sync.quietDuringLessons = p.getString("$s.quietDuringLessons", null)?.toBoolean() ?: false
ui.miniMenuVisible = p.getString("$s.miniDrawerVisible", null)?.toBoolean() ?: false
loginFinished = p.getString("$s.loginFinished", null)?.toBoolean() ?: false
sync.onlyWifi = p.getString("$s.registerSyncOnlyWifi", null)?.toBoolean() ?: false
sync.notifyAboutUpdates = p.getString("$s.notifyAboutUpdates", null)?.toBoolean() ?: true
timetable.bellSyncDiff = p.getString("$s.bellSyncDiff", null)?.let { Gson().fromJson(it, Time::class.java) }
sync.tokenMobidziennikList = listOf()
sync.tokenVulcanList = listOf()
sync.tokenLibrusList = listOf()
val tokens = p.getString("$s.fcmTokens", null)?.let { Gson().fromJson<Map<Int, Pair<String, List<Int>>>>(it, object: TypeToken<Map<Int, Pair<String, List<Int>>>>(){}.type) }
tokens?.forEach {
val token = it.value.first
when (it.key) {
LOGIN_TYPE_MOBIDZIENNIK -> sync.tokenMobidziennik = token
LOGIN_TYPE_VULCAN -> sync.tokenVulcan = token
LOGIN_TYPE_LIBRUS -> sync.tokenLibrus = token
}
}
dataVersion = 2
}
}}
private fun String?.fix(): String? {
return this?.replace("\"", "")?.let { if (it == "null") null else it }
}
}

View File

@ -37,6 +37,9 @@ fun AbstractConfig.set(key: String, value: JsonElement?) {
fun AbstractConfig.set(key: String, value: List<Any>?) {
set(key, value?.let { gson.toJson(it) })
}
fun AbstractConfig.set(key: String, value: Any?) {
set(key, value?.let { gson.toJson(it) })
}
fun AbstractConfig.setStringList(key: String, value: List<String>?) {
set(key, value?.let { gson.toJson(it) })
}
@ -74,6 +77,9 @@ fun HashMap<String, String?>.get(key: String, default: JsonObject?): JsonObject?
fun HashMap<String, String?>.get(key: String, default: JsonArray?): JsonArray? {
return this[key]?.let { JsonParser().parse(it)?.asJsonArray } ?: default
}
inline fun <reified T> HashMap<String, String?>.get(key: String, default: T?): T? {
return this[key]?.let { Gson().fromJson(it, T::class.java) } ?: default
}
/* !!! cannot use mutable list here - modifying it will not update the DB */
fun <T> HashMap<String, String?>.get(key: String, default: List<T>?, classOfT: Class<T>): List<T>? {
return this[key]?.let { ConfigGsonUtils().deserializeList<T>(gson, it, classOfT) } ?: default

View File

@ -5,32 +5,32 @@
package pl.szczodrzynski.edziennik.config.utils
import android.content.Context
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.HOUR
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_VULCAN
import pl.szczodrzynski.edziennik.config.Config
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.config.ConfigGrades.Companion.ORDER_BY_DATE_DESC
class ConfigMigration(app: App, config: Config) {
init { config.apply {
val p = app.getSharedPreferences("pl.szczodrzynski.edziennik_profiles", Context.MODE_PRIVATE)
val s = "app.appConfig"
if (dataVersion < 1) {
ui.theme = p.getString("$s.appTheme", null)?.toIntOrNull() ?: 1
sync.enabled = p.getString("$s.registerSyncEnabled", null)?.toBoolean() ?: true
sync.interval = p.getString("$s.registerSyncEnabled", null)?.toIntOrNull() ?: 3600
val oldButtons = p.getString("$s.miniDrawerButtonIds", null)?.let { str ->
str.replace("[\\[\\]]*".toRegex(), "")
.split(",\\s?".toRegex())
.mapNotNull { it.toIntOrNull() }
}
ui.miniMenuButtons = oldButtons ?: listOf(
val p = app.getSharedPreferences("pl.szczodrzynski.edziennik_profiles", Context.MODE_PRIVATE)
if (p.contains("app.appConfig.appTheme")) {
// migrate appConfig from app version 3.x and lower.
// Updates dataVersion to level 2.
AppConfigMigrationV3(p, config)
}
if (dataVersion < 2) {
appVersion = BuildConfig.VERSION_CODE
loginFinished = false
ui.language = null
ui.theme = 1
ui.appBackground = null
ui.headerBackground = null
ui.miniMenuVisible = false
ui.miniMenuButtons = listOf(
MainActivity.DRAWER_ITEM_HOME,
MainActivity.DRAWER_ITEM_TIMETABLE,
MainActivity.DRAWER_ITEM_AGENDA,
@ -39,46 +39,36 @@ class ConfigMigration(app: App, config: Config) {
MainActivity.DRAWER_ITEM_HOMEWORK,
MainActivity.DRAWER_ITEM_SETTINGS
)
dataVersion = 1
}
if (dataVersion < 2) {
devModePassword = p.getString("$s.devModePassword", null).fix()
sync.tokenApp = p.getString("$s.fcmToken", null).fix()
timetable.bellSyncMultiplier = p.getString("$s.bellSyncMultiplier", null)?.toIntOrNull() ?: 0
sync.quietHoursStart = p.getString("$s.quietHoursStart", null)?.toLongOrNull() ?: 0
appRateSnackbarTime = p.getString("$s.appRateSnackbarTime", null)?.toLongOrNull() ?: 0
sync.quietHoursEnd = p.getString("$s.quietHoursEnd", null)?.toLongOrNull() ?: 0
timetable.countInSeconds = p.getString("$s.countInSeconds", null)?.toBoolean() ?: false
ui.headerBackground = p.getString("$s.headerBackground", null).fix()
ui.appBackground = p.getString("$s.appBackground", null).fix()
ui.language = p.getString("$s.language", null).fix()
appVersion = p.getString("$s.lastAppVersion", null)?.toIntOrNull() ?: BuildConfig.VERSION_CODE
appInstalledTime = p.getString("$s.appInstalledTime", null)?.toLongOrNull() ?: 0
grades.orderBy = p.getString("$s.gradesOrderBy", null)?.toIntOrNull() ?: 0
sync.quietDuringLessons = p.getString("$s.quietDuringLessons", null)?.toBoolean() ?: false
ui.miniMenuVisible = p.getString("$s.miniDrawerVisible", null)?.toBoolean() ?: false
loginFinished = p.getString("$s.loginFinished", null)?.toBoolean() ?: false
sync.onlyWifi = p.getString("$s.registerSyncOnlyWifi", null)?.toBoolean() ?: false
sync.notifyAboutUpdates = p.getString("$s.notifyAboutUpdates", null)?.toBoolean() ?: true
timetable.bellSyncDiff = p.getString("$s.bellSyncDiff", null)?.let { Gson().fromJson(it, Time::class.java) }
sync.enabled = true
sync.interval = 1*HOUR.toInt()
sync.notifyAboutUpdates = true
sync.onlyWifi = false
sync.quietHoursStart = 0
sync.quietHoursEnd = 0
sync.quietDuringLessons = false
sync.tokenApp = null
sync.tokenMobidziennik = null
sync.tokenMobidziennikList = listOf()
sync.tokenVulcanList = listOf()
sync.tokenLibrus = null
sync.tokenLibrusList = listOf()
val tokens = p.getString("$s.fcmTokens", null)?.let { Gson().fromJson<Map<Int, Pair<String, List<Int>>>>(it, object: TypeToken<Map<Int, Pair<String, List<Int>>>>(){}.type) }
tokens?.forEach {
val token = it.value.first
when (it.key) {
LOGIN_TYPE_MOBIDZIENNIK -> sync.tokenMobidziennik = token
LOGIN_TYPE_VULCAN -> sync.tokenVulcan = token
LOGIN_TYPE_LIBRUS -> sync.tokenLibrus = token
}
}
sync.tokenVulcan = null
sync.tokenVulcanList = listOf()
timetable.bellSyncMultiplier = 0
timetable.bellSyncDiff = null
timetable.countInSeconds = false
grades.orderBy = ORDER_BY_DATE_DESC
dataVersion = 2
}
}}
private fun String?.fix(): String? {
return this?.replace("\"", "")?.let { if (it == "null") null else it }
}
if (dataVersion < 10) {
ui.openDrawerOnBackPressed = false
ui.homeCards = listOf()
ui.snowfall = false
ui.bottomSheetOpened = false
sync.dontShowAppManagerDialog = false
dataVersion = 10
}
}}
}

View File

@ -4,23 +4,21 @@
package pl.szczodrzynski.edziennik.config.utils
import android.content.Context
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.config.Config
import pl.szczodrzynski.edziennik.config.ProfileConfig
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.COLOR_MODE_WEIGHTED
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.YEAR_ALL_GRADES
class ProfileConfigMigration(app: App, config: Config) {
class ProfileConfigMigration(config: ProfileConfig) {
init { config.apply {
val p = app.getSharedPreferences("pl.szczodrzynski.edziennik_profiles", Context.MODE_PRIVATE)
val s = "app.appConfig"
if (dataVersion < 1) {
grades.colorMode = COLOR_MODE_WEIGHTED
grades.countZeroToAvg = true
grades.yearAverageMode = YEAR_ALL_GRADES
ui.agendaViewType = AGENDA_DEFAULT
//dataVersion = 1
}
if (dataVersion < 2) {
//gradesColorMode do profilu !
//agendaViewType do profilu !
// app.appConfig.dontCountZeroToAverage do profilu !
dataVersion = 1
}
}}
}

View File

@ -12,12 +12,15 @@ import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.*
import pl.szczodrzynski.edziennik.data.api.events.requests.ServiceCloseRequest
import pl.szczodrzynski.edziennik.data.api.events.requests.TaskCancelRequest
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.task.*
import pl.szczodrzynski.edziennik.data.api.task.ErrorReportTask
import pl.szczodrzynski.edziennik.data.api.task.IApiTask
import pl.szczodrzynski.edziennik.data.api.task.SzkolnyTask
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.utils.Utils.d
import kotlin.math.min
@ -26,7 +29,7 @@ import kotlin.math.roundToInt
class ApiService : Service() {
companion object {
const val TAG = "ApiService"
const val NOTIFICATION_API_CHANNEL_ID = "pl.szczodrzynski.edziennik.GET_DATA"
const val NOTIFICATION_API_CHANNEL_ID = "pl.szczodrzynski.edziennik.SYNC"
fun start(context: Context) {
context.startService(Intent(context, ApiService::class.java))
}
@ -40,15 +43,13 @@ class ApiService : Service() {
private val syncingProfiles = mutableListOf<Profile>()
private val finishingTaskQueue = mutableListOf(
SzkolnyTask.sync(syncingProfiles),
NotifyTask()
)
private val allTaskList = mutableListOf<IApiTask>()
private var szkolnyTaskFinished = false
private val allTaskRequestList = mutableListOf<Any>()
private val taskQueue = mutableListOf<IApiTask>()
private val errorList = mutableListOf<ApiError>()
private var serviceClosed = false
set(value) { field = value; notification.serviceClosed = value }
private var taskCancelled = false
private var taskIsRunning = false
private var taskRunning: IApiTask? = null // for debug purposes
@ -59,7 +60,7 @@ class ApiService : Service() {
private var taskProgress = -1f
private var taskProgressText: String? = null
private val notification by lazy { EdziennikNotification(this) }
private val notification by lazy { EdziennikNotification(app) }
private var lastEventTime = System.currentTimeMillis()
private var taskCancelTries = 0
@ -131,15 +132,20 @@ class ApiService : Service() {
checkIfTaskFrozen()
if (taskIsRunning)
return
if (taskCancelled || serviceClosed || (taskQueue.isEmpty() && finishingTaskQueue.isEmpty())) {
serviceClosed = false
if (taskCancelled || serviceClosed || (taskQueue.isEmpty() && szkolnyTaskFinished)) {
allCompleted()
return
}
lastEventTime = System.currentTimeMillis()
val task = if (taskQueue.isEmpty()) finishingTaskQueue.removeAt(0) else taskQueue.removeAt(0)
val task = if (taskQueue.isNotEmpty()) {
taskQueue.removeAt(0)
} else {
szkolnyTaskFinished = true
SzkolnyTask(app, syncingProfiles)
}
task.taskId = ++taskMaximumId
task.prepare(app)
taskIsRunning = true
@ -162,9 +168,8 @@ class ApiService : Service() {
try {
when (task) {
is EdziennikTask -> task.run(app, taskCallback)
is NotifyTask -> task.run(app, taskCallback)
is ErrorReportTask -> task.run(app, taskCallback, notification, errorList)
is SzkolnyTask -> task.run(app, taskCallback)
is SzkolnyTask -> task.run(taskCallback)
}
} catch (e: Exception) {
taskCallback.onError(ApiError(TAG, EXCEPTION_API_TASK).withThrowable(e))
@ -214,6 +219,7 @@ class ApiService : Service() {
}
private fun allCompleted() {
serviceClosed = true
EventBus.getDefault().postSticky(ApiTaskAllFinishedEvent())
stopSelf()
}
@ -229,10 +235,12 @@ class ApiService : Service() {
EventBus.getDefault().removeStickyEvent(task)
d(TAG, task.toString())
// fix for duplicated tasks, thank you EventBus
if (task in allTaskList)
return
allTaskList += task
if (task is EdziennikTask) {
// fix for duplicated tasks, thank you EventBus
if (task.request in allTaskRequestList)
return
allTaskRequestList += task.request
}
if (task is EdziennikTask) {
when (task.request) {
@ -293,11 +301,13 @@ class ApiService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
d(TAG, "Foreground service onStartCommand")
startForeground(EdziennikNotification.NOTIFICATION_ID, notification.notification)
startForeground(app.notifications.syncId, notification.notification)
return START_NOT_STICKY
}
override fun onDestroy() {
d(TAG, "Service destroyed")
serviceClosed = true
EventBus.getDefault().unregister(this)
}

View File

@ -1,209 +0,0 @@
package pl.szczodrzynski.edziennik.data.api
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ANNOUNCEMENTS
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ATTENDANCE
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_BEHAVIOUR
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_GRADES
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOME
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOMEWORK
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.models.Data
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Grade.*
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Notice
import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_LUCKY_NUMBER
import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_NEW_ANNOUNCEMENT
import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_NEW_ATTENDANCE
import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_NEW_EVENT
import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_NEW_GRADE
import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_NEW_HOMEWORK
import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_NEW_MESSAGE
import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_NEW_NOTICE
import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_TIMETABLE_LESSON_CHANGE
import pl.szczodrzynski.edziennik.getNotificationTitle
import pl.szczodrzynski.edziennik.utils.models.Date
class DataNotifications(val data: Data) {
companion object {
private const val TAG = "DataNotifications"
}
val app = data.app
val profileId = data.profile?.id ?: -1
val profileName = data.profile?.name ?: ""
val profile = data.profile
val loginStore = data.loginStore
init { run {
if (profile == null) {
return@run
}
val today = Date.getToday()
val todayValue = today.value
for (lesson in app.db.timetableDao().getNotNotifiedNow(profileId)) {
val text = app.getString(R.string.notification_lesson_change_format, lesson.getDisplayChangeType(app), if (lesson.displayDate == null) "" else lesson.displayDate!!.formattedString, lesson.changeSubjectName)
data.notifications += Notification(
title = app.getNotificationTitle(TYPE_TIMETABLE_LESSON_CHANGE),
text = text,
type = TYPE_TIMETABLE_LESSON_CHANGE,
profileId = profileId,
profileName = profileName,
viewId = DRAWER_ITEM_TIMETABLE,
addedDate = lesson.addedDate
).addExtra("timetableDate", lesson.displayDate?.stringY_m_d ?: "")
}
for (event in app.db.eventDao().getNotNotifiedNow(profileId)) {
val text = if (event.type == Event.TYPE_HOMEWORK)
app.getString(
if (event.subjectLongName.isNullOrEmpty())
R.string.notification_homework_no_subject_format
else
R.string.notification_homework_format,
event.subjectLongName,
event.eventDate.formattedString
)
else
app.getString(
if (event.subjectLongName.isNullOrEmpty())
R.string.notification_event_no_subject_format
else
R.string.notification_event_format,
event.typeName,
event.eventDate.formattedString,
event.subjectLongName
)
val type = if (event.type == Event.TYPE_HOMEWORK) TYPE_NEW_HOMEWORK else TYPE_NEW_EVENT
data.notifications += Notification(
title = app.getNotificationTitle(type),
text = text,
type = type,
profileId = profileId,
profileName = profileName,
viewId = if (event.type == Event.TYPE_HOMEWORK) DRAWER_ITEM_HOMEWORK else DRAWER_ITEM_AGENDA,
addedDate = event.addedDate
).addExtra("eventId", event.id).addExtra("eventDate", event.eventDate.value.toLong())
}
for (grade in app.db.gradeDao().getNotNotifiedNow(profileId)) {
val gradeName = when (grade.type) {
TYPE_SEMESTER1_PROPOSED, TYPE_SEMESTER2_PROPOSED -> app.getString(R.string.grade_semester_proposed_format_2, grade.name)
TYPE_SEMESTER1_FINAL, TYPE_SEMESTER2_FINAL -> app.getString(R.string.grade_semester_final_format_2, grade.name)
TYPE_YEAR_PROPOSED -> app.getString(R.string.grade_year_proposed_format_2, grade.name)
TYPE_YEAR_FINAL -> app.getString(R.string.grade_year_final_format_2, grade.name)
else -> grade.name
}
val text = app.getString(R.string.notification_grade_format, gradeName, grade.subjectLongName)
data.notifications += Notification(
title = app.getNotificationTitle(TYPE_NEW_GRADE),
text = text,
type = TYPE_NEW_GRADE,
profileId = profileId,
profileName = profileName,
viewId = DRAWER_ITEM_GRADES,
addedDate = grade.addedDate
).addExtra("gradeId", grade.id).addExtra("gradesSubjectId", grade.subjectId)
}
for (notice in app.db.noticeDao().getNotNotifiedNow(profileId)) {
val noticeTypeStr = if (notice.type == Notice.TYPE_POSITIVE) app.getString(R.string.notification_notice_praise) else if (notice.type == Notice.TYPE_NEGATIVE) app.getString(R.string.notification_notice_warning) else app.getString(R.string.notification_notice_new)
val text = app.getString(R.string.notification_notice_format, noticeTypeStr, notice.teacherFullName, Date.fromMillis(notice.addedDate).formattedString)
data.notifications += Notification(
title = app.getNotificationTitle(TYPE_NEW_NOTICE),
text = text,
type = TYPE_NEW_NOTICE,
profileId = profileId,
profileName = profileName,
viewId = DRAWER_ITEM_BEHAVIOUR,
addedDate = notice.addedDate
).addExtra("noticeId", notice.id)
}
for (attendance in app.db.attendanceDao().getNotNotifiedNow(profileId)) {
var attendanceTypeStr = app.getString(R.string.notification_type_attendance)
when (attendance.type) {
Attendance.TYPE_ABSENT -> attendanceTypeStr = app.getString(R.string.notification_absence)
Attendance.TYPE_ABSENT_EXCUSED -> attendanceTypeStr = app.getString(R.string.notification_absence_excused)
Attendance.TYPE_BELATED -> attendanceTypeStr = app.getString(R.string.notification_belated)
Attendance.TYPE_BELATED_EXCUSED -> attendanceTypeStr = app.getString(R.string.notification_belated_excused)
Attendance.TYPE_RELEASED -> attendanceTypeStr = app.getString(R.string.notification_release)
}
val text = app.getString(
if (attendance.subjectLongName.isNullOrEmpty())
R.string.notification_attendance_no_lesson_format
else
R.string.notification_attendance_format,
attendanceTypeStr,
attendance.subjectLongName,
attendance.lessonDate.formattedString
)
data.notifications += Notification(
title = app.getNotificationTitle(TYPE_NEW_ATTENDANCE),
text = text,
type = TYPE_NEW_ATTENDANCE,
profileId = profileId,
profileName = profileName,
viewId = DRAWER_ITEM_ATTENDANCE,
addedDate = attendance.addedDate
).addExtra("attendanceId", attendance.id).addExtra("attendanceSubjectId", attendance.subjectId)
}
for (announcement in app.db.announcementDao().getNotNotifiedNow(profileId)) {
val text = app.context.getString(R.string.notification_announcement_format, announcement.subject)
data.notifications += Notification(
title = app.getNotificationTitle(TYPE_NEW_ANNOUNCEMENT),
text = text,
type = TYPE_NEW_ANNOUNCEMENT,
profileId = profileId,
profileName = profileName,
viewId = DRAWER_ITEM_ANNOUNCEMENTS,
addedDate = announcement.addedDate
).addExtra("announcementId", announcement.id)
}
for (message in app.db.messageDao().getReceivedNotNotifiedNow(profileId)) {
val text = app.context.getString(R.string.notification_message_format, message.senderFullName, message.subject)
data.notifications += Notification(
title = app.getNotificationTitle(TYPE_NEW_MESSAGE),
text = text,
type = TYPE_NEW_MESSAGE,
profileId = profileId,
profileName = profileName,
viewId = DRAWER_ITEM_MESSAGES,
addedDate = message.addedDate
).addExtra("messageType", Message.TYPE_RECEIVED.toLong()).addExtra("messageId", message.id)
}
val luckyNumbers = app.db.luckyNumberDao().getNotNotifiedNow(profileId)
luckyNumbers?.removeAll { it.date < today }
luckyNumbers?.forEach { luckyNumber ->
val text = when (luckyNumber.date.value) {
todayValue -> // LN for today
app.getString(if (profile.studentNumber != -1 && profile.studentNumber == luckyNumber.number) R.string.notification_lucky_number_yours_format else R.string.notification_lucky_number_format, luckyNumber.number)
todayValue + 1 -> // LN for tomorrow
app.getString(if (profile.studentNumber != -1 && profile.studentNumber == luckyNumber.number) R.string.notification_lucky_number_yours_tomorrow_format else R.string.notification_lucky_number_tomorrow_format, luckyNumber.number)
else -> // LN for later
app.getString(if (profile.studentNumber != -1 && profile.studentNumber == luckyNumber.number) R.string.notification_lucky_number_yours_later_format else R.string.notification_lucky_number_later_format, luckyNumber.date.formattedString, luckyNumber.number)
}
data.notifications += Notification(
title = app.getNotificationTitle(TYPE_LUCKY_NUMBER),
text = text,
type = TYPE_LUCKY_NUMBER,
profileId = profileId,
profileName = profileName,
viewId = DRAWER_ITEM_HOME,
addedDate = luckyNumber.addedDate
)
}
data.db.metadataDao().setAllNotified(profileId, true)
}}
}

View File

@ -11,19 +11,16 @@ import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.PRIORITY_MIN
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import kotlin.math.roundToInt
class EdziennikNotification(val context: Context) {
companion object {
const val NOTIFICATION_ID = 20191001
}
private val notificationManager by lazy { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
class EdziennikNotification(val app: App) {
private val notificationManager by lazy { app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
private val notificationBuilder: NotificationCompat.Builder by lazy {
NotificationCompat.Builder(context, ApiService.NOTIFICATION_API_CHANNEL_ID)
NotificationCompat.Builder(app, ApiService.NOTIFICATION_API_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setPriority(PRIORITY_MIN)
.setOngoing(true)
@ -35,39 +32,40 @@ class EdziennikNotification(val context: Context) {
private var errorCount = 0
private var criticalErrorCount = 0
var serviceClosed = false
private fun cancelPendingIntent(taskId: Int): PendingIntent {
val intent = Intent("pl.szczodrzynski.edziennik.SZKOLNY_MAIN")
intent.putExtra("task", "TaskCancelRequest")
intent.putExtra("taskId", taskId)
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) as PendingIntent
return PendingIntent.getBroadcast(app, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) as PendingIntent
}
private val closePendingIntent: PendingIntent
get() {
val intent = Intent("pl.szczodrzynski.edziennik.SZKOLNY_MAIN")
intent.putExtra("task", "ServiceCloseRequest")
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) as PendingIntent
return PendingIntent.getBroadcast(app, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) as PendingIntent
}
private fun errorCountText(): String? {
var result = ""
if (criticalErrorCount > 0) {
result += context.resources.getQuantityString(R.plurals.critical_errors_format, criticalErrorCount, criticalErrorCount)
result += app.resources.getQuantityString(R.plurals.critical_errors_format, criticalErrorCount, criticalErrorCount)
}
if (criticalErrorCount > 0 && errorCount > 0) {
result += ", "
}
if (errorCount > 0) {
result += context.resources.getQuantityString(R.plurals.normal_errors_format, errorCount, errorCount)
result += app.resources.getQuantityString(R.plurals.normal_errors_format, errorCount, errorCount)
}
return if (result.isEmpty()) null else result
}
fun setIdle(): EdziennikNotification {
notificationBuilder.setContentTitle(context.getString(R.string.edziennik_notification_api_title))
notificationBuilder.setContentTitle(app.getString(R.string.edziennik_notification_api_title))
notificationBuilder.setProgress(0, 0, false)
notificationBuilder.apply {
val str = context.getString(R.string.edziennik_notification_api_text)
val str = app.getString(R.string.edziennik_notification_api_text)
setStyle(NotificationCompat.BigTextStyle().bigText(str))
setContentText(str)
}
@ -81,7 +79,7 @@ class EdziennikNotification(val context: Context) {
}
fun setCriticalError(): EdziennikNotification {
criticalErrorCount++
notificationBuilder.setContentTitle(context.getString(R.string.edziennik_notification_api_error_title))
notificationBuilder.setContentTitle(app.getString(R.string.edziennik_notification_api_error_title))
notificationBuilder.setProgress(0, 0, false)
notificationBuilder.apply {
val str = errorCountText()
@ -118,7 +116,7 @@ class EdziennikNotification(val context: Context) {
notificationBuilder.addAction(
NotificationCompat.Action(
R.drawable.ic_notification,
context.getString(R.string.edziennik_notification_api_close),
app.getString(R.string.edziennik_notification_api_close),
closePendingIntent
))
return this
@ -128,13 +126,15 @@ class EdziennikNotification(val context: Context) {
notificationBuilder.addAction(
NotificationCompat.Action(
R.drawable.ic_notification,
context.getString(R.string.edziennik_notification_api_cancel),
app.getString(R.string.edziennik_notification_api_cancel),
cancelPendingIntent(taskId)
))
}
fun post() {
notificationManager.notify(NOTIFICATION_ID, notification)
if (serviceClosed)
return
notificationManager.notify(app.notifications.syncId, notification)
}
}

View File

@ -91,7 +91,6 @@ fun Data.prepareFor(loginMethods: List<LoginMethod>, loginMethodId: Int) {
possibleLoginMethods += it.loginMethodId
}
targetEndpointIds.clear()
targetLoginMethodIds.clear()
// check the login method for any dependencies

View File

@ -51,6 +51,7 @@ const val ERROR_REQUEST_FAILURE_SSL_ERROR = 63
const val ERROR_RESPONSE_EMPTY = 100
const val ERROR_LOGIN_DATA_MISSING = 101
const val ERROR_PROFILE_MISSING = 105
const val ERROR_PROFILE_ARCHIVED = 106
const val ERROR_INVALID_LOGIN_MODE = 110
const val ERROR_LOGIN_METHOD_NOT_SATISFIED = 111
const val ERROR_NOT_IMPLEMENTED = 112
@ -129,6 +130,8 @@ const val ERROR_MOBIDZIENNIK_WEB_NO_SESSION_VALUE = 216
const val ERROR_MOBIDZIENNIK_WEB_NO_SERVER_ID = 213
const val ERROR_MOBIDZIENNIK_WEB_INVALID_RESPONSE = 214
const val ERROR_LOGIN_MOBIDZIENNIK_WEB_NO_SESSION_ID = 215
const val ERROR_LOGIN_MOBIDZIENNIK_API2_INVALID_LOGIN = 216
const val ERROR_LOGIN_MOBIDZIENNIK_API2_OTHER = 217
const val ERROR_LOGIN_VULCAN_INVALID_SYMBOL = 301
const val ERROR_LOGIN_VULCAN_INVALID_TOKEN = 302

View File

@ -41,6 +41,7 @@ internal const val FEATURE_PUSH_CONFIG = 120
object Features {
private fun getAllNecessary(): List<Int> = listOf(
FEATURE_ALWAYS_NEEDED,
FEATURE_PUSH_CONFIG,
FEATURE_STUDENT_INFO,
FEATURE_STUDENT_NUMBER,
FEATURE_SCHOOL_INFO,

View File

@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginMessages
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginPortal
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginSynergia
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLoginApi2
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLoginWeb
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginApi
import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginWeb
@ -86,11 +87,11 @@ const val LOGIN_METHOD_MOBIDZIENNIK_API2 = 300
val mobidziennikLoginMethods = listOf(
LoginMethod(LOGIN_TYPE_MOBIDZIENNIK, LOGIN_METHOD_MOBIDZIENNIK_WEB, MobidziennikLoginWeb::class.java)
.withIsPossible { _, _ -> true }
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }/*,
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED },
LoginMethod(LOGIN_TYPE_MOBIDZIENNIK, LOGIN_METHOD_MOBIDZIENNIK_API2, MobidziennikLoginApi2::class.java)
.withIsPossible { _, loginStore -> loginStore.hasLoginData("email") }
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }*/
.withIsPossible { profile, _ -> profile?.getStudentData("email", null) != null }
.withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }
)
const val LOGIN_TYPE_VULCAN = 4

View File

@ -56,7 +56,11 @@ object Regexes {
}
val MOBIDZIENNIK_MESSAGE_RECIPIENTS_JSON by lazy {
"""odbiorcy: (\[.+?\]),${'$'}""".toRegex(RegexOption.MULTILINE)
"""odbiorcy: (\[.+?]),${'$'}""".toRegex(RegexOption.MULTILINE)
}
val MOBIDZIENNIK_ACCOUNT_EMAIL by lazy {
"""name="email" value="(.+?@.+?\..+?)"""".toRegex(DOT_MATCHES_ALL)
}
@ -74,7 +78,7 @@ object Regexes {
"""id="ctl00_CzyRodzic" value="([01])" />""".toRegex()
}
val IDZIENNIK_LOGIN_FIRST_SCHOOL_YEAR by lazy {
"""name="ctl00\\${'$'}dxComboRokSzkolny".+?selected="selected".*?value="([0-9]+)">([0-9]+)/([0-9]+)<""".toRegex(DOT_MATCHES_ALL)
"""name="ctl00\${"$"}dxComboRokSzkolny".+?selected="selected".*?value="([0-9]+)">([0-9]+)/([0-9]+)<""".toRegex(DOT_MATCHES_ALL)
}
val IDZIENNIK_LOGIN_FIRST_STUDENT_SELECT by lazy {
"""<select.*?name="ctl00\${"$"}dxComboUczniowie".*?</select>""".toRegex(DOT_MATCHES_ALL)

View File

@ -1,4 +1,8 @@
package pl.szczodrzynski.edziennik.data.api.task
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-16.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik
import com.google.gson.JsonObject
import pl.szczodrzynski.edziennik.App
@ -12,11 +16,13 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.template.Template
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.Vulcan
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.task.IApiTask
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTask(profileId) {
companion object {
@ -57,6 +63,10 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa
private var edziennikInterface: EdziennikInterface? = null
internal fun run(app: App, taskCallback: EdziennikCallback) {
if (profile?.archived == true) {
taskCallback.onError(ApiError(TAG, ERROR_PROFILE_ARCHIVED))
return
}
edziennikInterface = when (loginStore.type) {
LOGIN_TYPE_LIBRUS -> Librus(app, profile, loginStore, taskCallback)
LOGIN_TYPE_MOBIDZIENNIK -> Mobidziennik(app, profile, loginStore, taskCallback)

View File

@ -7,11 +7,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_EDUDZIENNIK_WEB
import pl.szczodrzynski.edziennik.data.api.models.Data
import pl.szczodrzynski.edziennik.data.db.entity.EventType
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Subject
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.entity.*
/**
* Use http://patorjk.com/software/taag/#p=display&f=Big for the ascii art

View File

@ -15,12 +15,12 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.Edudzienn
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.utils.Utils.d
class Edudziennik(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
@ -41,9 +41,7 @@ class Edudziennik(val app: App, val profile: Profile?, val loginStore: LoginStor
private fun completed() {
data.saveData()
data.notify {
callback.onCompleted()
}
callback.onCompleted()
}
/* _______ _ _ _ _ _

View File

@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date
open class EdudziennikWeb(open val data: DataEdudziennik) {
companion object {
@ -24,11 +25,11 @@ open class EdudziennikWeb(open val data: DataEdudziennik) {
val profile
get() = data.profile
fun webGet(tag: String, endpoint: String, xhr: Boolean = false, onSuccess: (text: String) -> Unit) {
fun webGet(tag: String, endpoint: String, xhr: Boolean = false, semester: Int? = null, onSuccess: (text: String) -> Unit) {
val url = "https://dziennikel.appspot.com/" + when (endpoint.endsWith('/') || endpoint.contains('?') || endpoint.isEmpty()) {
true -> endpoint
else -> "$endpoint/"
}
} + (semester?.let { "?semester=" + if(it == -1) "all" else it } ?: "")
d(tag, "Request: Edudziennik/Web - $url")
@ -40,6 +41,18 @@ open class EdudziennikWeb(open val data: DataEdudziennik) {
return
}
if (semester == null && url.contains("start")) {
profile?.also { profile ->
val cookies = data.app.cookieJar.getForDomain("dziennikel.appspot.com")
val semesterCookie = cookies.firstOrNull { it.name() == "semester" }?.value()?.toIntOrNull()
semesterCookie?.let { data.currentSemester = it }
if (semesterCookie == 2 && profile.dateSemester2Start > Date.getToday())
profile.dateSemester2Start = Date.getToday().stepForward(0, 0, -1)
}
}
try {
onSuccess(text)
} catch (e: Exception) {
@ -67,11 +80,6 @@ open class EdudziennikWeb(open val data: DataEdudziennik) {
.name("sessionid")
.value(data.webSessionId!!)
.domain("dziennikel.appspot.com")
.secure().httpOnly().build(),
Cookie.Builder()
.name("semester")
.value((data.currentSemester).toString())
.domain("dziennikel.appspot.com")
.secure().httpOnly().build()
))

View File

@ -11,9 +11,9 @@ import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ATTENDANCE_TYPES
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.utils.models.Date
@ -26,7 +26,7 @@ class EdudziennikWebAttendance(override val data: DataEdudziennik,
}
init { data.profile?.also { profile ->
webGet(TAG, data.studentEndpoint + "Presence") { text ->
webGet(TAG, data.studentEndpoint + "Presence", semester = -1) { text ->
val attendanceTypes = EDUDZIENNIK_ATTENDANCE_TYPES.find(text)?.get(1)?.split(',')?.map {
val type = EDUDZIENNIK_ATTENDANCE_TYPE.find(it.trim())
@ -69,7 +69,7 @@ class EdudziennikWebAttendance(override val data: DataEdudziennik,
id,
lesson?.displayTeacherId ?: -1,
lesson?.displaySubjectId ?: -1,
data.currentSemester,
profile.currentSemester,
name,
date,
lesson?.displayStartTime ?: startTime,
@ -77,14 +77,16 @@ class EdudziennikWebAttendance(override val data: DataEdudziennik,
)
data.attendanceList.add(attendanceObject)
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_ATTENDANCE,
id,
profile.empty,
profile.empty,
System.currentTimeMillis()
))
if(type != Attendance.TYPE_PRESENT) {
data.metadataList.add(Metadata(
profileId,
Metadata.TYPE_ATTENDANCE,
id,
profile.empty,
profile.empty,
System.currentTimeMillis()
))
}
}
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE, SYNC_ALWAYS)

View File

@ -13,9 +13,9 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_EXAMS
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.utils.models.Date
@ -60,7 +60,7 @@ class EdudziennikWebExams(override val data: DataEdudziennik,
startTime,
topic,
-1,
eventType.id.toInt(),
eventType.id,
false,
-1,
subject.id,

View File

@ -13,10 +13,10 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.entity.Grade.*
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
@ -27,10 +27,18 @@ class EdudziennikWebGrades(override val data: DataEdudziennik,
private const val TAG = "EdudziennikWebGrades"
}
init { data.profile?.also { profile ->
webGet(TAG, data.studentEndpoint + "start") { text ->
val doc = Jsoup.parse(text)
private var requestSemester: Int? = null
init {
if (profile?.empty == true && data.currentSemester == 2) requestSemester = 1
getGrades()
}
private fun getGrades() { data.profile?.also { profile ->
webGet(TAG, data.studentEndpoint + "start", semester = requestSemester) { text ->
val semester = requestSemester ?: data.currentSemester
val doc = Jsoup.parse(text)
val subjects = doc.select("#student_grades tbody").firstOrNull()?.children()
subjects?.forEach { subjectElement ->
@ -107,7 +115,7 @@ class EdudziennikWebGrades(override val data: DataEdudziennik,
name,
value,
if (gradeCountToAverage) weight else 0f,
data.currentSemester,
semester,
teacher.id,
subject.id
).apply {
@ -137,7 +145,7 @@ class EdudziennikWebGrades(override val data: DataEdudziennik,
proposed,
proposed.toFloatOrNull() ?: 0f,
0f,
data.currentSemester,
semester,
-1,
subject.id
).apply {
@ -170,7 +178,7 @@ class EdudziennikWebGrades(override val data: DataEdudziennik,
final,
final.toFloatOrNull() ?: 0f,
0f,
data.currentSemester,
semester,
-1,
subject.id
).apply {
@ -201,12 +209,17 @@ class EdudziennikWebGrades(override val data: DataEdudziennik,
TYPE_SEMESTER1_FINAL,
TYPE_SEMESTER2_FINAL
).map {
DataRemoveModel.Grades.semesterWithType(data.currentSemester, it)
DataRemoveModel.Grades.semesterWithType(semester, it)
})
}
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_GRADES, SYNC_ALWAYS)
onSuccess()
if (profile.empty && requestSemester == 1 && data.currentSemester == 2) {
requestSemester = null
getGrades()
} else {
data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_GRADES, SYNC_ALWAYS)
onSuccess()
}
}
}}
} ?: onSuccess() }
}

View File

@ -10,9 +10,9 @@ import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_NOTE_ID
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_NOTES
import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Notice
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.utils.models.Date
@ -42,7 +42,7 @@ class EdudziennikWebNotes(override val data: DataEdudziennik,
profileId,
id,
description,
data.currentSemester,
profile.currentSemester,
Notice.TYPE_NEUTRAL,
teacher.id
)

View File

@ -13,7 +13,6 @@ import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.getUnixDate
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date
class EdudziennikLoginWeb(val data: DataEdudziennik, val onSuccess: () -> Unit) {
companion object {
@ -62,7 +61,6 @@ class EdudziennikLoginWeb(val data: DataEdudziennik, val onSuccess: () -> Unit)
val cookies = data.app.cookieJar.getForDomain("dziennikel.appspot.com")
val sessionId = cookies.firstOrNull { it.name() == "sessionid" }?.value()
val semester = cookies.firstOrNull { it.name() == "semester" }?.value()?.toIntOrNull()
if (sessionId == null) {
data.error(ApiError(TAG, ERROR_LOGIN_EDUDZIENNIK_WEB_NO_SESSION_ID)
@ -72,14 +70,6 @@ class EdudziennikLoginWeb(val data: DataEdudziennik, val onSuccess: () -> Unit)
}
data.webSessionId = sessionId
if (data.profile != null && semester != null) {
data.currentSemester = semester
if (semester == 2 && data.profile.dateSemester2Start > Date.getToday())
data.profile.dateSemester2Start = Date.getToday().stepForward(0, 0, -1)
}
data.webSessionIdExpiryTime = response.getUnixDate() + 45 * 60 /* 45 min */
onSuccess()
}

View File

@ -17,12 +17,12 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.idziennik.login.IdziennikLo
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.utils.Utils.d
class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
@ -43,9 +43,7 @@ class Idziennik(val app: App, val profile: Profile?, val loginStore: LoginStore,
private fun completed() {
data.saveData()
data.notify {
callback.onCompleted()
}
callback.onCompleted()
}
/* _______ _ _ _ _ _

View File

@ -19,12 +19,12 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLogin
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.utils.Utils.d
class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
@ -45,9 +45,7 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va
private fun completed() {
data.saveData()
data.notify {
callback.onCompleted()
}
callback.onCompleted()
}
/* _______ _ _ _ _ _

View File

@ -9,10 +9,9 @@ import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_ATTENDANCES
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.entity.Attendance
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.utils.models.Date
class LibrusApiAttendances(override val data: DataLibrus,
@ -33,11 +32,12 @@ class LibrusApiAttendances(override val data: DataLibrus,
val attendances = json.getJsonArray("Attendances")?.asJsonObjectList()
attendances?.forEach { attendance ->
val id = Utils.strToInt((attendance.getString("Id") ?: return@forEach)
val id = ((attendance.getString("Id") ?: return@forEach)
.replace("[^\\d.]".toRegex(), "")).toLong()
val lessonId = attendance.getJsonObject("Lesson")?.getLong("Id") ?: -1
val lessonNo = attendance.getInt("LessonNo") ?: return@forEach
val lessonDate = Date.fromY_m_d(attendance.getString("Date"))
val teacherId = attendance.getJsonObject("AddedBy")?.getLong("Id")
val semester = attendance.getInt("Semester") ?: return@forEach
val type = attendance.getJsonObject("Type")?.getLong("Id") ?: return@forEach
val typeObject = data.attendanceTypes.get(type)
@ -52,7 +52,7 @@ class LibrusApiAttendances(override val data: DataLibrus,
val attendanceObject = Attendance(
profileId,
id,
lesson?.teacherId ?: -1,
teacherId ?: lesson?.teacherId ?: -1,
lesson?.subjectId ?: -1,
semester,
topic,

View File

@ -25,9 +25,10 @@ class LibrusApiClassrooms(override val data: DataLibrus,
val id = classroom.getLong("Id") ?: return@forEach
val name = classroom.getString("Name")?.toLowerCase(Locale.getDefault()) ?: ""
val symbol = classroom.getString("Symbol")?.toLowerCase(Locale.getDefault()) ?: ""
val nameShort = name.split(" ").onEach { it[0] }.joinToString()
val nameShort = name.fixWhiteSpaces().split(" ").onEach { it[0] }.joinToString()
val symbolParts = symbol.fixWhiteSpaces().split(" ")
val friendlyName = if (name != symbol && !name.contains(symbol) && !nameShort.contains(symbol)) {
val friendlyName = if (name != symbol && !name.contains(symbol) && !name.containsAll(symbolParts) && !nameShort.contains(symbol)) {
classroom.getString("Symbol") + " " + classroom.getString("Name")
}
else {

View File

@ -10,9 +10,9 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_EVENTS
import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
@ -34,7 +34,7 @@ class LibrusApiEvents(override val data: DataLibrus,
val id = event.getLong("Id") ?: return@forEach
val eventDate = Date.fromY_m_d(event.getString("Date"))
val topic = event.getString("Content") ?: ""
val type = event.getJsonObject("Category")?.getInt("Id") ?: -1
val type = event.getJsonObject("Category")?.getLong("Id") ?: -1
val teacherId = event.getJsonObject("CreatedBy")?.getLong("Id") ?: -1
val subjectId = event.getJsonObject("Subject")?.getLong("Id") ?: -1
val teamId = event.getJsonObject("Class")?.getLong("Id") ?: -1

View File

@ -23,6 +23,10 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da
&& webSessionKey.isNotNullNorEmpty()
&& webServerId.isNotNullNorEmpty()
fun isApi2LoginValid() = loginEmail.isNotNullNorEmpty()
&& loginId.isNotNullNorEmpty()
&& globalId.isNotNullNorEmpty()
override fun satisfyLoginMethods() {
loginMethods.clear()
if (isWebLoginValid()) {
@ -44,11 +48,6 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da
get() { mLoginServerName = mLoginServerName ?: loginStore.getLoginData("serverName", null); return mLoginServerName }
set(value) { loginStore.putLoginData("serverName", value); mLoginServerName = value }
private var mLoginEmail: String? = null
var loginEmail: String?
get() { mLoginEmail = mLoginEmail ?: loginStore.getLoginData("email", null); return mLoginEmail }
set(value) { loginStore.putLoginData("email", value); mLoginEmail = value }
private var mLoginUsername: String? = null
var loginUsername: String?
get() { mLoginUsername = mLoginUsername ?: loginStore.getLoginData("username", null); return mLoginUsername }
@ -90,6 +89,48 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da
get() { mWebSessionIdExpiryTime = mWebSessionIdExpiryTime ?: loginStore.getLoginData("sessionIDTime", 0L); return mWebSessionIdExpiryTime ?: 0L }
set(value) { loginStore.putLoginData("sessionIDTime", value); mWebSessionIdExpiryTime = value }
/* _____ _____ ___
/\ | __ \_ _| |__ \
/ \ | |__) || | ) |
/ /\ \ | ___/ | | / /
/ ____ \| | _| |_ / /_
/_/ \_\_| |_____| |___*/
/**
* A global ID (whatever it is) used in API 2
* and Firebase push from Mobidziennik.
*/
var globalId: String?
get() { mGlobalId = mGlobalId ?: profile?.getStudentData("globalId", null); return mGlobalId }
set(value) { profile?.putStudentData("globalId", value) ?: return; mGlobalId = value }
private var mGlobalId: String? = null
/**
* User's email that may or may not
* be retrieved from Web by [MobidziennikWebAccountEmail].
* Used to log in to API 2.
*/
var loginEmail: String?
get() { mLoginEmail = mLoginEmail ?: profile?.getStudentData("email", null); return mLoginEmail }
set(value) { profile?.putStudentData("email", value); mLoginEmail = value }
private var mLoginEmail: String? = null
/**
* A login ID used in the API 2.
* Looks more or less like "7063@2019@zslpoznan".
*/
var loginId: String?
get() { mLoginId = mLoginId ?: profile?.getStudentData("loginId", null); return mLoginId }
set(value) { profile?.putStudentData("loginId", value) ?: return; mLoginId = value }
private var mLoginId: String? = null
/**
* No need to explain.
*/
var ciasteczkoAutoryzacji: String?
get() { mCiasteczkoAutoryzacji = mCiasteczkoAutoryzacji ?: profile?.getStudentData("ciasteczkoAutoryzacji", null); return mCiasteczkoAutoryzacji }
set(value) { profile?.putStudentData("ciasteczkoAutoryzacji", value) ?: return; mCiasteczkoAutoryzacji = value }
private var mCiasteczkoAutoryzacji: String? = null
override fun saveData() {
super.saveData()

View File

@ -17,12 +17,12 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.Mobidzie
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.utils.Utils.d
class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
@ -45,9 +45,7 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto
private fun completed() {
data.saveData()
data.notify {
callback.onCompleted()
}
callback.onCompleted()
}
/* _______ _ _ _ _ _

View File

@ -27,11 +27,11 @@ val MobidziennikFeatures = listOf(
), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)), // TODO divide features into separate view IDs (all with API_MAIN)
// push config
/*Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_PUSH_CONFIG, listOf(
Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_PUSH_CONFIG, listOf(
ENDPOINT_MOBIDZIENNIK_API2_MAIN to LOGIN_METHOD_MOBIDZIENNIK_API2
), listOf(LOGIN_METHOD_MOBIDZIENNIK_API2)).withShouldSync { data ->
data.app.appConfig.fcmTokens[LOGIN_TYPE_MOBIDZIENNIK]?.second?.contains(data.profileId) == false
},*/
!data.app.config.sync.tokenMobidziennikList.contains(data.profileId)
},

View File

@ -7,10 +7,8 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.*
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api.MobidziennikApi
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebCalendar
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebGrades
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebMessagesAll
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.MobidziennikWebMessagesInbox
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api2.MobidziennikApi2Main
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.*
import pl.szczodrzynski.edziennik.utils.Utils
class MobidziennikData(val data: DataMobidziennik, val onSuccess: () -> Unit) {
@ -44,6 +42,10 @@ class MobidziennikData(val data: DataMobidziennik, val onSuccess: () -> Unit) {
data.startProgress(R.string.edziennik_progress_endpoint_data)
MobidziennikApi(data, onSuccess)
}
ENDPOINT_MOBIDZIENNIK_API2_MAIN -> {
data.startProgress(R.string.edziennik_progress_endpoint_push_config)
MobidziennikApi2Main(data, onSuccess)
}
ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_INBOX -> {
data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox)
MobidziennikWebMessagesInbox(data) { onSuccess() }
@ -59,6 +61,10 @@ class MobidziennikData(val data: DataMobidziennik, val onSuccess: () -> Unit) {
ENDPOINT_MOBIDZIENNIK_WEB_GRADES -> {
data.startProgress(R.string.edziennik_progress_endpoint_grades)
MobidziennikWebGrades(data) { onSuccess() }
}
ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL -> {
data.startProgress(R.string.edziennik_progress_endpoint_account_details)
MobidziennikWebAccountEmail(data) { onSuccess() }
}/*
ENDPOINT_MOBIDZIENNIK_WEB_NOTICES -> {
data.startProgress(R.string.edziennik_progress_endpoint_behaviour)

View File

@ -0,0 +1,91 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-12.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api2
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.JsonCallbackHandler
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_API2_MAIN
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLoginApi2
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getJsonObject
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.Utils
class MobidziennikApi2Main(val data: DataMobidziennik,
val onSuccess: () -> Unit) {
companion object {
private const val TAG = "MobidziennikApi2Main"
}
val profileId
get() = data.profile?.id ?: -1
val profile
get() = data.profile
init {
Utils.d(TAG, "Request: Mobidziennik/Api2/Main - https://${data.loginServerName}.mobidziennik.pl/api2/logowanie")
val callback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (json == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
return
}
json.getJsonObject("error")?.let {
val text = it.getString("type") ?: it.getString("message")
when (text) {
"LOGIN_ERROR" -> ERROR_LOGIN_MOBIDZIENNIK_API2_INVALID_LOGIN
// TODO other error types
else -> ERROR_LOGIN_MOBIDZIENNIK_API2_OTHER
}.let { errorCode ->
data.error(ApiError(TAG, errorCode)
.withApiResponse(text)
.withResponse(response))
return
}
}
val user = json.getJsonObject("user")
data.ciasteczkoAutoryzacji = user.getString("auth_key")
// sync always: this endpoint has .shouldSync set
data.setSyncNext(ENDPOINT_MOBIDZIENNIK_API2_MAIN, SYNC_ALWAYS)
data.app.config.sync.tokenMobidziennikList =
data.app.config.sync.tokenMobidziennikList + profileId
onSuccess()
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(TAG, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url("https://${data.loginServerName}.mobidziennik.pl/api2/logowanie")
.userAgent(MOBIDZIENNIK_USER_AGENT)
.contentType("application/x-www-form-urlencoded; charset=UTF-8")
.addParameter("login", data.loginId)
.addParameter("email", data.loginEmail)
.addParameter("haslo", data.loginPassword)
.addParameter("device", MobidziennikLoginApi2.getDevice(data.app).toString())
.apply {
data.ciasteczkoAutoryzacji?.let { addParameter("ciasteczko_autoryzacji", it) }
}
.post()
.callback(callback)
.build()
.enqueue()
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-21.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web
import pl.szczodrzynski.edziennik.DAY
import pl.szczodrzynski.edziennik.data.api.Regexes
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb
import pl.szczodrzynski.edziennik.get
class MobidziennikWebAccountEmail(override val data: DataMobidziennik,
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
companion object {
private const val TAG = "MobidziennikWebAccountEmail"
}
init {
webGet(TAG, "/dziennik/edytujprofil") { text ->
MobidziennikLuckyNumberExtractor(data, text)
val email = Regexes.MOBIDZIENNIK_ACCOUNT_EMAIL.find(text)?.let { it[1] }
data.loginEmail = email
data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL, if (email == null) 3*DAY else 7*DAY)
onSuccess()
}
}
}

View File

@ -9,16 +9,16 @@ import pl.szczodrzynski.edziennik.data.api.Regexes
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_CALENDAR
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.getString
import pl.szczodrzynski.edziennik.utils.Utils.crc16
import pl.szczodrzynski.edziennik.utils.models.Date
import java.util.*
class MobidziennikWebCalendar(override val data: DataMobidziennik,
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
companion object {
private const val TAG = "MobidziennikWebCalendar"
}

View File

@ -12,9 +12,9 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.Mobidzien
import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.singleOrNull
@ -25,7 +25,7 @@ import pl.szczodrzynski.edziennik.utils.models.Time
class MobidziennikWebGetMessage(
override val data: DataMobidziennik,
private val message: MessageFull,
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
companion object {
private const val TAG = "MobidziennikWebGetMessage"
}

View File

@ -11,9 +11,9 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidzienn
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb
import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.fixWhiteSpaces
import pl.szczodrzynski.edziennik.get
import pl.szczodrzynski.edziennik.singleOrNull
@ -21,7 +21,7 @@ import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
class MobidziennikWebGrades(override val data: DataMobidziennik,
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
companion object {
private const val TAG = "MobidziennikWebGrades"
}

View File

@ -19,7 +19,7 @@ import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.utils.models.Date
class MobidziennikWebMessagesAll(override val data: DataMobidziennik,
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
companion object {
private const val TAG = "MobidziennikWebMessagesAll"
}

View File

@ -8,16 +8,16 @@ import org.jsoup.Jsoup
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_INBOX
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.fixName
import pl.szczodrzynski.edziennik.singleOrNull
import pl.szczodrzynski.edziennik.utils.models.Date
class MobidziennikWebMessagesInbox(override val data: DataMobidziennik,
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
companion object {
private const val TAG = "MobidziennikWebMessagesInbox"
}

View File

@ -10,7 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.Mobidzien
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
class MobidziennikWebNotices(override val data: DataMobidziennik,
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
val onSuccess: () -> Unit) : MobidziennikWeb(data) {
companion object {
private const val TAG = "MobidziennikWebNotices"
}

View File

@ -52,7 +52,7 @@ class MobidziennikLogin(val data: DataMobidziennik, val onSuccess: () -> Unit) {
}
LOGIN_METHOD_MOBIDZIENNIK_API2 -> {
data.startProgress(R.string.edziennik_progress_login_mobidziennik_api2)
//MobidziennikLoginApi2(data) { onSuccess(loginMethodId) }
MobidziennikLoginApi2(data) { onSuccess(loginMethodId) }
}
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-12.
*/
package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login
import android.os.Build
import com.google.gson.JsonObject
import im.wangchao.mhttp.Request
import im.wangchao.mhttp.Response
import im.wangchao.mhttp.callback.JsonCallbackHandler
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.*
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.utils.Utils
class MobidziennikLoginApi2(val data: DataMobidziennik, val onSuccess: () -> Unit) {
companion object {
private const val TAG = "MobidziennikLoginApi2"
fun getDevice(app: App) = JsonObject(
"available" to true,
"platform" to "Android",
"version" to Build.VERSION.RELEASE,
"uuid" to app.deviceId,
"cordova" to "7.1.2",
"model" to "${Build.MANUFACTURER} ${Build.MODEL}",
"manufacturer" to "Aplikacja Szkolny.eu",
"isVirtual" to false,
"serial" to try { System.getProperty("ro.serialno") ?: System.getProperty("ro.boot.serialno") } catch (_: Exception) { Build.UNKNOWN },
"appVersion" to "10.6, 2020.01.09-12.15.53",
"pushRegistrationId" to app.config.sync.tokenMobidziennik
)
}
init { run {
if (data.isApi2LoginValid()) {
onSuccess()
}
else {
if (data.loginServerName.isNotNullNorEmpty() && data.loginEmail.isNotNullNorEmpty() && data.loginPassword.isNotNullNorEmpty()) {
loginWithCredentials()
}
else {
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
}
}
}}
private fun loginWithCredentials() {
Utils.d(TAG, "Request: Mobidziennik/Login/Api2 - https://mobidziennik.pl/logowanie")
val callback = object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response?) {
if (json == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
return
}
json.getJsonObject("error")?.let {
val text = it.getString("type") ?: it.getString("message")
when (text) {
"LOGIN_ERROR" -> ERROR_LOGIN_MOBIDZIENNIK_API2_INVALID_LOGIN
// TODO other error types
else -> ERROR_LOGIN_MOBIDZIENNIK_API2_OTHER
}.let { errorCode ->
data.error(ApiError(TAG, errorCode)
.withApiResponse(text)
.withResponse(response))
return
}
}
data.loginEmail = json.getString("email")
data.globalId = json.getString("id_global")
data.loginId = json.getString("login")
onSuccess()
}
override fun onFailure(response: Response?, throwable: Throwable?) {
data.error(ApiError(TAG, ERROR_REQUEST_FAILURE)
.withResponse(response)
.withThrowable(throwable))
}
}
Request.builder()
.url("https://mobidziennik.pl/logowanie")
.userAgent(MOBIDZIENNIK_USER_AGENT)
.contentType("application/x-www-form-urlencoded; charset=UTF-8")
.addParameter("api2", true)
.addParameter("email", data.loginEmail)
.addParameter("haslo", data.loginPassword)
.addParameter("device", getDevice(data.app).toString())
.post()
.callback(callback)
.build()
.enqueue()
}
}

View File

@ -15,12 +15,12 @@ import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.prepare
import pl.szczodrzynski.edziennik.data.api.templateLoginMethods
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.utils.Utils.d
class Template(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
@ -40,9 +40,7 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore,
private fun completed() {
data.saveData()
data.notify {
callback.onCompleted()
}
callback.onCompleted()
}
/* _______ _ _ _ _ _

View File

@ -18,12 +18,12 @@ import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.prepare
import pl.szczodrzynski.edziennik.data.api.prepareFor
import pl.szczodrzynski.edziennik.data.api.vulcanLoginMethods
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.utils.Utils.d
class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface {
@ -44,9 +44,7 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va
private fun completed() {
data.saveData()
data.notify {
callback.onCompleted()
}
callback.onCompleted()
}
/* _______ _ _ _ _ _

View File

@ -4,13 +4,15 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.api
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_MESSAGES_CHANGE_STATUS
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent
import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_SENT
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
class VulcanApiMessagesChangeStatus(
override val data: DataVulcan,
@ -22,40 +24,42 @@ class VulcanApiMessagesChangeStatus(
}
init {
data.profile?.also { profile ->
apiGet(TAG, VULCAN_API_ENDPOINT_MESSAGES_CHANGE_STATUS, parameters = mapOf(
"WiadomoscId" to messageObject.id,
"FolderWiadomosci" to "Odebrane",
"Status" to "Widoczna",
"LoginId" to data.studentLoginId,
"IdUczen" to data.studentId
)) { _, _ ->
apiGet(TAG, VULCAN_API_ENDPOINT_MESSAGES_CHANGE_STATUS, parameters = mapOf(
"WiadomoscId" to messageObject.id,
"FolderWiadomosci" to "Odebrane",
"Status" to "Widoczna",
"LoginId" to data.studentLoginId,
"IdUczen" to data.studentId
)) { _, _ ->
if (!messageObject.seen) {
data.setSeenMetadataList.add(Metadata(
profileId,
Metadata.TYPE_MESSAGE,
messageObject.id,
true,
true,
messageObject.addedDate
))
}
if (!messageObject.seen) {
data.setSeenMetadataList.add(Metadata(
profileId,
Metadata.TYPE_MESSAGE,
messageObject.id,
true,
true,
messageObject.addedDate
))
if (messageObject.type != TYPE_SENT) {
val messageRecipientObject = MessageRecipient(
profileId,
-1,
-1,
System.currentTimeMillis(),
messageObject.id
)
data.messageRecipientList.add(messageRecipientObject)
}
onSuccess()
messageObject.seen = true
}
if (messageObject.type != TYPE_SENT) {
val messageRecipientObject = MessageRecipient(
profileId,
-1,
-1,
System.currentTimeMillis(),
messageObject.id
)
data.messageRecipientList.add(messageRecipientObject)
}
EventBus.getDefault().postSticky(MessageGetEvent(messageObject))
onSuccess()
}
}
}

View File

@ -9,12 +9,8 @@ import pl.szczodrzynski.edziennik.data.api.VULCAN_API_ENDPOINT_MESSAGES_RECEIVED
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_API_MESSAGES_INBOX
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanApi
import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.*
import pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED
import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.text.replace
@ -88,7 +84,7 @@ class VulcanApiMessagesInbox(override val data: DataVulcan, val onSuccess: () ->
data.messageIgnoreList.add(messageObject)
data.messageRecipientList.add(messageRecipientObject)
data.metadataList.add(Metadata(
data.setSeenMetadataList.add(Metadata(
profileId,
Metadata.TYPE_MESSAGE,
id,

View File

@ -99,7 +99,7 @@ class VulcanApiMessagesSent(override val data: DataVulcan, val onSuccess: () ->
)
data.messageIgnoreList.add(messageObject)
data.metadataList.add(Metadata(
data.setSeenMetadataList.add(Metadata(
profileId,
Metadata.TYPE_MESSAGE,
id,

View File

@ -0,0 +1,9 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-21.
*/
package pl.szczodrzynski.edziennik.data.api.events
import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage
data class FeedbackMessageEvent(val message: FeedbackMessage)

View File

@ -0,0 +1,7 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-18.
*/
package pl.szczodrzynski.edziennik.data.api.events
class ProfileListEmptyEvent

View File

@ -93,7 +93,7 @@ class ApiError(val tag: String, var errorCode: Int) {
stackTrace = throwable?.stackTraceString,
request = requestString,
response = responseString,
apiResponse = apiResponse,
apiResponse = apiResponse ?: response?.parserErrorBody,
isCritical = isCritical
)
}

View File

@ -194,7 +194,6 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
if (profile == null)
return // return on first login
profile.empty = false
profile.userCode = generateUserCode()
db.profileDao().add(profile)
@ -294,21 +293,6 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
db.messageRecipientDao().addAllIgnore(messageRecipientIgnoreList)
}
fun notify(onSuccess: () -> Unit) {
if (profile == null) {
onSuccess()
return
}
try {
DataNotifications(this)
db.notificationDao().addAll(notifications)
onSuccess()
} catch (e: Exception) {
error(ApiError(TAG, EXCEPTION_NOTIFY)
.withThrowable(e))
}
}
fun setSyncNext(endpointId: Int, syncIn: Long? = null, viewId: Int? = null, syncAt: Long? = null) {
EndpointTimer(profile?.id
?: -1, endpointId).apply {
@ -366,8 +350,8 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
apiError.errorCode = when (apiError.throwable) {
is UnknownHostException -> ERROR_REQUEST_FAILURE_HOSTNAME_NOT_FOUND
is SSLException -> ERROR_REQUEST_FAILURE_SSL_ERROR
is InterruptedIOException, is ConnectException -> ERROR_REQUEST_FAILURE_NO_INTERNET
is SocketTimeoutException -> ERROR_REQUEST_FAILURE_TIMEOUT
is InterruptedIOException, is ConnectException -> ERROR_REQUEST_FAILURE_NO_INTERNET
else ->
if (apiError.errorCode == ERROR_REQUEST_FAILURE)
when (apiError.response?.code()) {

View File

@ -47,11 +47,11 @@ open class DataRemoveModel {
}
}
class Events(private val type: Int?, private val exceptType: Int?, private val exceptTypes: List<Int>?) : DataRemoveModel() {
class Events(private val type: Long?, private val exceptType: Long?, private val exceptTypes: List<Long>?) : DataRemoveModel() {
companion object {
fun futureExceptType(exceptType: Int) = Events(null, exceptType, null)
fun futureExceptTypes(exceptTypes: List<Int>) = Events(null, null, exceptTypes)
fun futureWithType(type: Int) = Events(type, null, null)
fun futureExceptType(exceptType: Long) = Events(null, exceptType, null)
fun futureExceptTypes(exceptTypes: List<Long>) = Events(null, null, exceptTypes)
fun futureWithType(type: Long) = Events(type, null, null)
}
fun commit(profileId: Int, dao: EventDao) {

View File

@ -1,52 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2019-12-13
*/
package pl.szczodrzynski.edziennik.data.api.szkolny
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Profile
class Szkolny(val app: App, val callback: EdziennikCallback) {
private val api = SzkolnyApi(app)
fun sync(profileList: List<Profile>) {
val profiles = profileList.filter { it.registration == Profile.REGISTRATION_ENABLED }
if (profiles.isNotEmpty()) {
val events = api.getEvents(profiles)
if (events.isNotEmpty()) {
app.db.eventDao().addAll(events)
app.db.metadataDao().addAllIgnore(events.map { event ->
Metadata(
event.profileId,
Metadata.TYPE_EVENT,
event.id,
event.seen,
event.notified,
event.addedDate
)
})
}
}
completed()
}
/*fun shareEvent(event: EventFull) {
api.shareEvent(event)
completed()
}
fun unshareEvent(event: EventFull) {
api.unshareEvent(event)
completed()
}*/
private fun completed() {
callback.onCompleted()
}
}

View File

@ -12,15 +12,15 @@ import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter
import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.TimeAdapter
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor
import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.request.EventShareRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.request.ServerSyncRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.request.WebPushRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.request.*
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage
import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.md5
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
@ -56,40 +56,41 @@ class SzkolnyApi(val app: App) {
api = retrofit.create()
}
fun getEvents(profiles: List<Profile>): List<EventFull> {
private fun getDevice() = run {
val config = app.config
val device = Device(
osType = "Android",
osVersion = Build.VERSION.RELEASE,
hardware = "${Build.MANUFACTURER} ${Build.MODEL}",
pushToken = app.config.sync.tokenApp,
appVersion = BuildConfig.VERSION_NAME,
appType = BuildConfig.BUILD_TYPE,
appVersionCode = BuildConfig.VERSION_CODE,
syncInterval = app.config.sync.interval
)
device.toString().md5().let {
if (it == config.hash)
null
else {
config.hash = it
device
}
}
}
fun getEvents(profiles: List<Profile>, notifications: List<Notification>, blacklistedIds: List<Long>): List<EventFull> {
val teams = app.db.teamDao().allNow
val notifications = app.db.notificationDao().getNotPostedNow()
val response = api.serverSync(ServerSyncRequest(
deviceId = app.deviceId,
device = run {
val config = app.config
val device = ServerSyncRequest.Device(
osType = "Android",
osVersion = Build.VERSION.RELEASE,
hardware = "${Build.MANUFACTURER} ${Build.MODEL}",
pushToken = app.config.sync.tokenApp,
appVersion = BuildConfig.VERSION_NAME,
appType = BuildConfig.BUILD_TYPE,
appVersionCode = BuildConfig.VERSION_CODE,
syncInterval = app.config.sync.interval
)
device.toString().md5().let {
if (it == config.hash)
null
else {
config.hash = it
device
}
}
},
device = getDevice(),
userCodes = profiles.map { it.userCode },
users = profiles.mapNotNull { profile ->
val config = app.config.getFor(profile.id)
val user = ServerSyncRequest.User(
profile.userCode,
profile.studentNameLong ?: "",
profile.studentNameShort ?: "",
profile.studentNameLong,
profile.studentNameShort,
profile.loginStoreType,
teams.filter { it.profileId == profile.id }.map { it.code }
)
@ -108,17 +109,19 @@ class SzkolnyApi(val app: App) {
val events = mutableListOf<EventFull>()
response?.data?.events?.forEach { event ->
teams.filter { it.code == event.teamCode }.forEach { team ->
val profile = profiles.firstOrNull { it.id == team.profileId }
if (event.id in blacklistedIds)
return@forEach
teams.filter { it.code == event.teamCode }.onEach { team ->
val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@onEach
events.add(event.apply {
events.add(EventFull(event).apply {
profileId = team.profileId
teamId = team.id
addedManually = true
seen = profile?.empty ?: false
notified = profile?.empty ?: false
seen = profile.empty
notified = profile.empty
if (profile?.userCode == event.sharedBy) sharedBy = "self"
if (profile.userCode == event.sharedBy) sharedBy = "self"
})
}
}
@ -131,6 +134,7 @@ class SzkolnyApi(val app: App) {
return api.shareEvent(EventShareRequest(
deviceId = app.deviceId,
device = getDevice(),
sharedByName = event.sharedByName,
shareTeamCode = team.code,
event = event
@ -142,6 +146,7 @@ class SzkolnyApi(val app: App) {
return api.shareEvent(EventShareRequest(
deviceId = app.deviceId,
device = getDevice(),
sharedByName = event.sharedByName,
unshareTeamCode = team.code,
eventId = event.id
@ -154,8 +159,9 @@ class SzkolnyApi(val app: App) {
fun pairBrowser(browserId: String?, pairToken: String?, onError: ((List<ApiResponse.Error>) -> Unit)? = null): List<WebPushResponse.Browser> {
val response = api.webPush(WebPushRequest(
action = "pairBrowser",
deviceId = app.deviceId,
device = getDevice(),
action = "pairBrowser",
browserId = browserId,
pairToken = pairToken
)).execute().body()
@ -170,8 +176,9 @@ class SzkolnyApi(val app: App) {
fun listBrowsers(onError: ((List<ApiResponse.Error>) -> Unit)? = null): List<WebPushResponse.Browser> {
val response = api.webPush(WebPushRequest(
action = "listBrowsers",
deviceId = app.deviceId
deviceId = app.deviceId,
device = getDevice(),
action = "listBrowsers"
)).execute().body()
return response?.data?.browsers ?: emptyList()
@ -179,8 +186,9 @@ class SzkolnyApi(val app: App) {
fun unpairBrowser(browserId: String): List<WebPushResponse.Browser> {
val response = api.webPush(WebPushRequest(
action = "unpairBrowser",
deviceId = app.deviceId,
device = getDevice(),
action = "unpairBrowser",
browserId = browserId
)).execute().body()
@ -190,7 +198,31 @@ class SzkolnyApi(val app: App) {
fun errorReport(errors: List<ErrorReportRequest.Error>): ApiResponse<Nothing>? {
return api.errorReport(ErrorReportRequest(
deviceId = app.deviceId,
device = getDevice(),
appVersion = BuildConfig.VERSION_NAME,
errors = errors
)).execute().body()
}
fun unregisterAppUser(userCode: String): ApiResponse<Nothing>? {
return api.appUser(AppUserRequest(
deviceId = app.deviceId,
device = getDevice(),
userCode = userCode
)).execute().body()
}
fun getUpdate(channel: String): ApiResponse<List<Update>>? {
return api.updates(channel).execute().body()
}
fun sendFeedbackMessage(senderName: String?, targetDeviceId: String?, text: String): FeedbackMessage? {
return api.feedbackMessage(FeedbackMessageRequest(
deviceId = app.deviceId,
device = getDevice(),
senderName = senderName,
targetDeviceId = targetDeviceId,
text = text
)).execute().body()?.data?.message
}
}

View File

@ -4,16 +4,13 @@
package pl.szczodrzynski.edziennik.data.api.szkolny
import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.request.EventShareRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.request.ServerSyncRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.request.WebPushRequest
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse
import pl.szczodrzynski.edziennik.data.api.szkolny.response.ServerSyncResponse
import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse
import pl.szczodrzynski.edziennik.data.api.szkolny.request.*
import pl.szczodrzynski.edziennik.data.api.szkolny.response.*
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
interface SzkolnyService {
@ -28,4 +25,13 @@ interface SzkolnyService {
@POST("errorReport")
fun errorReport(@Body request: ErrorReportRequest): Call<ApiResponse<Nothing>>
@POST("appUser")
fun appUser(@Body request: AppUserRequest): Call<ApiResponse<Nothing>>
@GET("updates/app")
fun updates(@Query("channel") channel: String = "release"): Call<ApiResponse<List<Update>>>
@POST("feedbackMessage")
fun feedbackMessage(@Body request: FeedbackMessageRequest): Call<ApiResponse<FeedbackMessageResponse>>
}

View File

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

View File

@ -0,0 +1,13 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-18.
*/
package pl.szczodrzynski.edziennik.data.api.szkolny.request
data class AppUserRequest(
val deviceId: String,
val device: Device? = null,
val action: String = "unregister",
val userCode: String
)

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-20.
*/
package pl.szczodrzynski.edziennik.data.api.szkolny.request
data class Device(
val osType: String,
val osVersion: String,
val hardware: String,
val pushToken: String?,
val appVersion: String,
val appType: String,
val appVersionCode: Int,
val syncInterval: Int
)

View File

@ -6,6 +6,9 @@ package pl.szczodrzynski.edziennik.data.api.szkolny.request
data class ErrorReportRequest(
val deviceId: String,
val device: Device? = null,
val appVersion: String,
val errors: List<Error>
) {
data class Error(
@ -20,4 +23,4 @@ data class ErrorReportRequest(
val apiResponse: String?,
val isCritical: Boolean
)
}
}

View File

@ -7,9 +7,10 @@ package pl.szczodrzynski.edziennik.data.api.szkolny.request
import pl.szczodrzynski.edziennik.data.db.full.EventFull
data class EventShareRequest (
val deviceId: String,
val device: Device? = null,
val action: String = "event",
val deviceId: String,
val sharedByName: String,
val shareTeamCode: String? = null,

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-21.
*/
package pl.szczodrzynski.edziennik.data.api.szkolny.request
data class FeedbackMessageRequest(
val deviceId: String,
val device: Device? = null,
val senderName: String?,
val targetDeviceId: String?,
val text: String
)

View File

@ -5,7 +5,6 @@
package pl.szczodrzynski.edziennik.data.api.szkolny.request
data class ServerSyncRequest(
val deviceId: String,
val device: Device? = null,
@ -14,17 +13,6 @@ data class ServerSyncRequest(
val notifications: List<Notification>? = null
) {
data class Device(
val osType: String,
val osVersion: String,
val hardware: String,
val pushToken: String?,
val appVersion: String,
val appType: String,
val appVersionCode: Int,
val syncInterval: Int
)
data class User(
val userCode: String,
val studentName: String,

View File

@ -5,10 +5,11 @@
package pl.szczodrzynski.edziennik.data.api.szkolny.request
data class WebPushRequest(
val deviceId: String,
val device: Device? = null,
val action: String,
val deviceId: String,
val browserId: String? = null,
val pairToken: String? = null
)
)

View File

@ -0,0 +1,9 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-21.
*/
package pl.szczodrzynski.edziennik.data.api.szkolny.response
import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage
data class FeedbackMessageResponse(val message: FeedbackMessage)

View File

@ -0,0 +1,15 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-18.
*/
package pl.szczodrzynski.edziennik.data.api.szkolny.response
data class Update(
val versionCode: Int,
val versionName: String,
val releaseDate: String,
val releaseNotes: String?,
val releaseType: String,
val isOnGooglePlay: Boolean,
val downloadUrl: String?
)

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-17.
*/
package pl.szczodrzynski.edziennik.data.api.task
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.data.db.entity.Profile
class AppSync(val app: App, val notifications: MutableList<Notification>, val profiles: List<Profile>, val api: SzkolnyApi) {
companion object {
private const val TAG = "AppSync"
}
/**
* Run the app sync, sending all pending notifications
* and retrieving a list of shared events.
*
* Events are automatically saved to app database,
* along with corresponding metadata objects.
*
* @return a number of events inserted to DB, possibly needing a notification
*/
fun run(): Int {
val profiles = profiles.filter { it.registration == Profile.REGISTRATION_ENABLED && !it.archived }
if (profiles.isNotEmpty()) {
val blacklistedIds = app.db.eventDao().blacklistedIds;
val events = api.getEvents(profiles, notifications, blacklistedIds)
if (events.isNotEmpty()) {
app.db.metadataDao().addAllIgnore(events.map { event ->
Metadata(
event.profileId,
Metadata.TYPE_EVENT,
event.id,
event.seen,
event.notified,
event.addedDate
)
})
return app.db.eventDao().addAll(events).size
}
}
return 0;
}
}

View File

@ -39,4 +39,18 @@ abstract class IApiTask(open val profileId: Int) {
override fun toString(): String {
return "IApiTask(profileId=$profileId, taskId=$taskId, profile=$profile, taskName=$taskName)"
}
companion object {
fun enqueueAll(context: Context, tasks: List<IApiTask>) {
Intent(context, ApiService::class.java).let {
if (SDK_INT >= O)
context.startForegroundService(it)
else
context.startService(it)
}
tasks.forEach {
EventBus.getDefault().postSticky(it)
}
}
}
}

View File

@ -0,0 +1,277 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-1-16.
*/
package pl.szczodrzynski.edziennik.data.api.task
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.*
import pl.szczodrzynski.edziennik.getNotificationTitle
import pl.szczodrzynski.edziennik.utils.models.Date
class Notifications(val app: App, val notifications: MutableList<Notification>, val profiles: List<Profile>) {
companion object {
private const val TAG = "Notifications"
}
private val today by lazy { Date.getToday() }
private val todayValue by lazy { today.value }
/**
* Create a [Notification] from every possible
* data type. Notifications are posted whenever
* the object's metadata `notified` property is
* set to false.
*/
fun run() {
timetableNotifications()
eventNotifications()
gradeNotifications()
behaviourNotifications()
attendanceNotifications()
announcementNotifications()
messageNotifications()
luckyNumberNotifications()
}
private fun timetableNotifications() {
for (lesson in app.db.timetableDao().getNotNotifiedNow()) {
val text = app.getString(
R.string.notification_lesson_change_format,
lesson.getDisplayChangeType(app),
if (lesson.displayDate == null) "" else lesson.displayDate!!.formattedString,
lesson.changeSubjectName
)
notifications += Notification(
id = Notification.buildId(lesson.profileId, Notification.TYPE_TIMETABLE_LESSON_CHANGE, lesson.id),
title = app.getNotificationTitle(Notification.TYPE_TIMETABLE_LESSON_CHANGE),
text = text,
type = Notification.TYPE_TIMETABLE_LESSON_CHANGE,
profileId = lesson.profileId,
profileName = profiles.singleOrNull { it.id == lesson.profileId }?.name,
viewId = MainActivity.DRAWER_ITEM_TIMETABLE,
addedDate = lesson.addedDate
).addExtra("timetableDate", lesson.displayDate?.stringY_m_d ?: "")
}
}
private fun eventNotifications() {
for (event in app.db.eventDao().notNotifiedNow) {
val text = if (event.type == Event.TYPE_HOMEWORK)
app.getString(
if (event.subjectLongName.isNullOrEmpty())
R.string.notification_homework_no_subject_format
else
R.string.notification_homework_format,
event.subjectLongName,
event.eventDate.formattedString
)
else
app.getString(
if (event.subjectLongName.isNullOrEmpty())
R.string.notification_event_no_subject_format
else
R.string.notification_event_format,
event.typeName,
event.eventDate.formattedString,
event.subjectLongName
)
val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT
notifications += Notification(
id = Notification.buildId(event.profileId, type, event.id),
title = app.getNotificationTitle(type),
text = text,
type = type,
profileId = event.profileId,
profileName = profiles.singleOrNull { it.id == event.profileId }?.name,
viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA,
addedDate = event.addedDate
).addExtra("eventId", event.id).addExtra("eventDate", event.eventDate.value.toLong())
}
}
fun sharedEventNotifications() {
for (event in app.db.eventDao().notNotifiedNow.filter { it.sharedBy != null }) {
val text = app.getString(
R.string.notification_shared_event_format,
event.sharedByName,
event.typeName ?: "wydarzenie",
event.eventDate.formattedString,
event.topic
)
val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT
notifications += Notification(
id = Notification.buildId(event.profileId, type, event.id),
title = app.getNotificationTitle(type),
text = text,
type = type,
profileId = event.profileId,
profileName = profiles.singleOrNull { it.id == event.profileId }?.name,
viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA,
addedDate = event.addedDate
).addExtra("eventId", event.id).addExtra("eventDate", event.eventDate.value.toLong())
}
}
private fun gradeNotifications() {
for (grade in app.db.gradeDao().notNotifiedNow) {
val gradeName = when (grade.type) {
Grade.TYPE_SEMESTER1_PROPOSED, Grade.TYPE_SEMESTER2_PROPOSED -> app.getString(R.string.grade_semester_proposed_format_2, grade.name)
Grade.TYPE_SEMESTER1_FINAL, Grade.TYPE_SEMESTER2_FINAL -> app.getString(R.string.grade_semester_final_format_2, grade.name)
Grade.TYPE_YEAR_PROPOSED -> app.getString(R.string.grade_year_proposed_format_2, grade.name)
Grade.TYPE_YEAR_FINAL -> app.getString(R.string.grade_year_final_format_2, grade.name)
else -> grade.name
}
val text = app.getString(
R.string.notification_grade_format,
gradeName,
grade.subjectLongName
)
notifications += Notification(
id = Notification.buildId(grade.profileId, Notification.TYPE_NEW_GRADE, grade.id),
title = app.getNotificationTitle(Notification.TYPE_NEW_GRADE),
text = text,
type = Notification.TYPE_NEW_GRADE,
profileId = grade.profileId,
profileName = profiles.singleOrNull { it.id == grade.profileId }?.name,
viewId = MainActivity.DRAWER_ITEM_GRADES,
addedDate = grade.addedDate
).addExtra("gradeId", grade.id).addExtra("gradesSubjectId", grade.subjectId)
}
}
private fun behaviourNotifications() {
for (notice in app.db.noticeDao().notNotifiedNow) {
val noticeTypeStr = when (notice.type) {
Notice.TYPE_POSITIVE -> app.getString(R.string.notification_notice_praise)
Notice.TYPE_NEGATIVE -> app.getString(R.string.notification_notice_warning)
else -> app.getString(R.string.notification_notice_new)
}
val text = app.getString(
R.string.notification_notice_format,
noticeTypeStr,
notice.teacherFullName,
Date.fromMillis(notice.addedDate).formattedString
)
notifications += Notification(
id = Notification.buildId(notice.profileId, Notification.TYPE_NEW_NOTICE, notice.id),
title = app.getNotificationTitle(Notification.TYPE_NEW_NOTICE),
text = text,
type = Notification.TYPE_NEW_NOTICE,
profileId = notice.profileId,
profileName = profiles.singleOrNull { it.id == notice.profileId }?.name,
viewId = MainActivity.DRAWER_ITEM_BEHAVIOUR,
addedDate = notice.addedDate
).addExtra("noticeId", notice.id)
}
}
private fun attendanceNotifications() {
for (attendance in app.db.attendanceDao().notNotifiedNow) {
val attendanceTypeStr = when (attendance.type) {
Attendance.TYPE_ABSENT -> app.getString(R.string.notification_absence)
Attendance.TYPE_ABSENT_EXCUSED -> app.getString(R.string.notification_absence_excused)
Attendance.TYPE_BELATED -> app.getString(R.string.notification_belated)
Attendance.TYPE_BELATED_EXCUSED -> app.getString(R.string.notification_belated_excused)
Attendance.TYPE_RELEASED -> app.getString(R.string.notification_release)
Attendance.TYPE_DAY_FREE -> app.getString(R.string.notification_day_free)
else -> app.getString(R.string.notification_type_attendance)
}
val text = app.getString(
if (attendance.subjectLongName.isNullOrEmpty())
R.string.notification_attendance_no_lesson_format
else
R.string.notification_attendance_format,
attendanceTypeStr,
attendance.subjectLongName,
attendance.lessonDate.formattedString
)
notifications += Notification(
id = Notification.buildId(attendance.profileId, Notification.TYPE_NEW_ATTENDANCE, attendance.id),
title = app.getNotificationTitle(Notification.TYPE_NEW_ATTENDANCE),
text = text,
type = Notification.TYPE_NEW_ATTENDANCE,
profileId = attendance.profileId,
profileName = profiles.singleOrNull { it.id == attendance.profileId }?.name,
viewId = MainActivity.DRAWER_ITEM_ATTENDANCE,
addedDate = attendance.addedDate
).addExtra("attendanceId", attendance.id).addExtra("attendanceSubjectId", attendance.subjectId)
}
}
private fun announcementNotifications() {
for (announcement in app.db.announcementDao().notNotifiedNow) {
val text = app.getString(
R.string.notification_announcement_format,
announcement.teacherFullName,
announcement.subject
)
notifications += Notification(
id = Notification.buildId(announcement.profileId, Notification.TYPE_NEW_ANNOUNCEMENT, announcement.id),
title = app.getNotificationTitle(Notification.TYPE_NEW_ANNOUNCEMENT),
text = text,
type = Notification.TYPE_NEW_ANNOUNCEMENT,
profileId = announcement.profileId,
profileName = profiles.singleOrNull { it.id == announcement.profileId }?.name,
viewId = MainActivity.DRAWER_ITEM_ANNOUNCEMENTS,
addedDate = announcement.addedDate
).addExtra("announcementId", announcement.id)
}
}
private fun messageNotifications() {
for (message in app.db.messageDao().receivedNotNotifiedNow) {
val text = app.getString(
R.string.notification_message_format,
message.senderFullName,
message.subject
)
notifications += Notification(
id = Notification.buildId(message.profileId, Notification.TYPE_NEW_MESSAGE, message.id),
title = app.getNotificationTitle(Notification.TYPE_NEW_MESSAGE),
text = text,
type = Notification.TYPE_NEW_MESSAGE,
profileId = message.profileId,
profileName = profiles.singleOrNull { it.id == message.profileId }?.name,
viewId = MainActivity.DRAWER_ITEM_MESSAGES,
addedDate = message.addedDate
).addExtra("messageType", Message.TYPE_RECEIVED.toLong()).addExtra("messageId", message.id)
}
}
private fun luckyNumberNotifications() {
val luckyNumbers = app.db.luckyNumberDao().notNotifiedNow
luckyNumbers?.removeAll { it.date < today }
luckyNumbers?.forEach { luckyNumber ->
val profile = profiles.singleOrNull { it.id == luckyNumber.profileId } ?: return@forEach
val text = when (profile.studentNumber != -1 && profile.studentNumber == luckyNumber.number) {
true -> when (luckyNumber.date.value) {
todayValue -> R.string.notification_lucky_number_yours_format
todayValue + 1 -> R.string.notification_lucky_number_yours_tomorrow_format
else -> R.string.notification_lucky_number_yours_later_format
}
else -> when (luckyNumber.date.value) {
todayValue -> R.string.notification_lucky_number_format
todayValue + 1 -> R.string.notification_lucky_number_tomorrow_format
else -> R.string.notification_lucky_number_later_format
}
}
notifications += Notification(
id = Notification.buildId(luckyNumber.profileId, Notification.TYPE_LUCKY_NUMBER, luckyNumber.date.value.toLong()),
title = app.getNotificationTitle(Notification.TYPE_LUCKY_NUMBER),
text = app.getString(text, luckyNumber.date.formattedString, luckyNumber.number),
type = Notification.TYPE_LUCKY_NUMBER,
profileId = luckyNumber.profileId,
profileName = profile.name,
viewId = MainActivity.DRAWER_ITEM_HOME,
addedDate = luckyNumber.addedDate
)
}
}
}

View File

@ -1,90 +0,0 @@
package pl.szczodrzynski.edziennik.data.api.task
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.Notifier.ID_NOTIFICATIONS
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.getNotificationTitle
import pl.szczodrzynski.edziennik.utils.models.Notification
import kotlin.math.min
class NotifyTask : IApiTask(-1) {
override fun prepare(app: App) {
taskName = app.getString(R.string.edziennik_notification_api_notify_title)
}
override fun cancel() {
}
fun run(app: App, taskCallback: EdziennikCallback) {
val list = app.db.notificationDao().getNotPostedNow()
val notificationList = list.subList(0, min(10, list.size))
val unreadCount = list.size
for (notification in notificationList) {
val intent = Intent(app, MainActivity::class.java)
notification.fillIntent(intent)
val pendingIntent = PendingIntent.getActivity(app, notification.id, intent, 0)
val notificationBuilder = NotificationCompat.Builder(app, app.notifier.notificationGroup)
// title, text, type, date
.setContentTitle(notification.profileName)
.setContentText(notification.text)
.setSubText(app.getNotificationTitle(notification.type))
.setWhen(notification.addedDate)
.setTicker(app.getString(R.string.notification_ticker_format, Notification.stringType(app, notification.type)))
// icon, color, lights, priority
.setSmallIcon(R.drawable.ic_notification)
.setColor(app.notifier.notificationColor)
.setLights(-0xff0001, 2000, 2000)
.setPriority(app.notifier.notificationPriority)
// channel, group, style
.setChannelId(app.notifier.notificationGroup)
.setGroup(app.notifier.notificationGroup)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setStyle(NotificationCompat.BigTextStyle().bigText(notification.text))
// intent, auto cancel
.setContentIntent(pendingIntent)
.setAutoCancel(true)
if (!app.notifier.shouldBeQuiet()) {
notificationBuilder.setDefaults(app.notifier.notificationDefaults)
}
app.notifier.notificationManager.notify(notification.id, notificationBuilder.build())
}
if (notificationList.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val intent = Intent(app, MainActivity::class.java)
intent.action = "android.intent.action.MAIN"
intent.putExtra("fragmentId", MainActivity.DRAWER_ITEM_NOTIFICATIONS)
val pendingIntent = PendingIntent.getActivity(app, ID_NOTIFICATIONS,
intent, 0)
val groupBuilder = NotificationCompat.Builder(app, app.notifier.notificationGroup)
.setSmallIcon(R.drawable.ic_notification)
.setColor(app.notifier.notificationColor)
.setContentTitle(app.getString(R.string.notification_new_notification_title_format, unreadCount))
.setGroupSummary(true)
.setAutoCancel(true)
.setChannelId(app.notifier.notificationGroup)
.setGroup(app.notifier.notificationGroup)
.setLights(-0xff0001, 2000, 2000)
.setPriority(app.notifier.notificationPriority)
.setContentIntent(pendingIntent)
.setStyle(NotificationCompat.BigTextStyle())
if (!app.notifier.shouldBeQuiet()) {
groupBuilder.setDefaults(app.notifier.notificationDefaults)
}
app.notifier.notificationManager.notify(ID_NOTIFICATIONS, groupBuilder.build())
}
app.db.notificationDao().setAllPosted()
taskCallback.onCompleted()
}
}

View File

@ -0,0 +1,172 @@
package pl.szczodrzynski.edziennik.data.api.task
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.os.Build
import android.util.SparseIntArray
import androidx.core.app.NotificationCompat
import androidx.core.util.forEach
import androidx.core.util.set
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_SERVER_MESSAGE
import pl.szczodrzynski.edziennik.data.db.entity.Notification as AppNotification
class PostNotifications(val app: App, nList: List<AppNotification>) {
companion object {
private const val TAG = "PostNotifications"
}
/*public boolean shouldBeQuiet() {
long now = Time.getNow().getInMillis();
long start = app.config.getSync().getQuietHoursStart();
long end = app.config.getSync().getQuietHoursEnd();
if (start > end) {
end += 1000 * 60 * 60 * 24;
//Log.d(TAG, "Night passing");
}
if (start > now) {
now += 1000 * 60 * 60 * 24;
//Log.d(TAG, "Now is smaller");
}
//Log.d(TAG, "Start is "+start+", now is "+now+", end is "+end);
return start > 0 && now >= start && now <= end;
}*/
fun shouldBeQuiet() = false
private fun buildSummaryText(summaryCounts: SparseIntArray): CharSequence {
val summaryTexts = mutableListOf<String>()
summaryCounts.forEach { key, value ->
if (value <= 0)
return@forEach
val pluralRes = when (key) {
AppNotification.TYPE_TIMETABLE_LESSON_CHANGE -> R.plurals.notification_new_timetable_change_format
AppNotification.TYPE_NEW_GRADE -> R.plurals.notification_new_grades_format
AppNotification.TYPE_NEW_EVENT -> R.plurals.notification_new_events_format
AppNotification.TYPE_NEW_HOMEWORK -> R.plurals.notification_new_homework_format
AppNotification.TYPE_NEW_SHARED_EVENT -> R.plurals.notification_new_shared_events_format
AppNotification.TYPE_NEW_SHARED_HOMEWORK -> R.plurals.notification_new_shared_homework_format
AppNotification.TYPE_NEW_MESSAGE -> R.plurals.notification_new_messages_format
AppNotification.TYPE_NEW_NOTICE -> R.plurals.notification_new_notices_format
AppNotification.TYPE_NEW_ATTENDANCE -> R.plurals.notification_new_attendance_format
AppNotification.TYPE_LUCKY_NUMBER -> R.plurals.notification_new_lucky_number_format
AppNotification.TYPE_NEW_ANNOUNCEMENT -> R.plurals.notification_new_announcements_format
else -> R.plurals.notification_other_format
}
summaryTexts += app.resources.getQuantityString(pluralRes, value, value)
}
return summaryTexts.concat(", ")
}
init { run {
val count = nList.size
if (count == 0)
return@run
val notificationManager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val summaryCounts = SparseIntArray()
val newNotificationsText = app.resources.getQuantityString(R.plurals.notification_count_format, count, count)
val newNotificationsShortText = app.resources.getQuantityString(R.plurals.notification_count_short_format, count, count)
val intent = Intent(
app,
MainActivity::class.java,
"fragmentId" to MainActivity.DRAWER_ITEM_NOTIFICATIONS
)
val summaryIntent = PendingIntent.getActivity(app, app.notifications.dataId, intent, PendingIntent.FLAG_ONE_SHOT)
// On Nougat or newer - show maximum 8 notifications
// On Marshmallow or older - show maximum 4 notifications
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N && count > 4 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && count > 8) {
val summaryList = mutableListOf<CharSequence>()
nList.forEach {
summaryCounts[it.type]++
summaryList += listOf(
it.profileName.asBoldSpannable(),
it.text
).concat(": ")
}
// Create a summary to show *instead* of notifications
val combined = NotificationCompat.Builder(app, app.notifications.dataKey)
.setContentTitle(app.getString(R.string.app_name))
.setContentText(buildSummaryText(summaryCounts))
.setTicker(newNotificationsText)
.setSmallIcon(R.drawable.ic_notification)
.setStyle(NotificationCompat.InboxStyle()
.also {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
it.setBigContentTitle(app.getString(R.string.app_name))
it.setSummaryText(newNotificationsShortText)
}
else {
it.setBigContentTitle(newNotificationsText)
it.setSummaryText(app.getString(R.string.notification_click_to_see_all))
}
summaryList.forEach { line ->
it.addLine(line)
}
})
.setColor(0xff2196f3.toInt())
.setLights(0xff2196f3.toInt(), 2000, 2000)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setGroup(app.notifications.dataKey)
.setContentIntent(summaryIntent)
.setAutoCancel(true)
.build()
notificationManager.notify(System.currentTimeMillis().toInt(), combined)
}
else {
// Less than 8 notifications
val notifications = nList.map {
summaryCounts[it.type]++
NotificationCompat.Builder(app, app.notifications.dataKey)
.setContentTitle(it.profileName ?: app.getString(R.string.app_name))
.setContentText(it.text)
.setSubText(if (it.type == TYPE_SERVER_MESSAGE) null else it.title)
.setTicker("${it.profileName}: ${it.title}")
.setSmallIcon(R.drawable.ic_notification)
.setStyle(NotificationCompat.BigTextStyle()
.bigText(it.text))
.setWhen(it.addedDate)
.setColor(0xff2196f3.toInt())
.setLights(0xff2196f3.toInt(), 2000, 2000)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setGroup(app.notifications.dataKey)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setContentIntent(it.getPendingIntent(app))
.setAutoCancel(true)
.build()
}
val time = System.currentTimeMillis()
notificationManager.apply {
notifications.forEachIndexed { index, it ->
notificationManager.notify((time + index).toInt(), it)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val summary = NotificationCompat.Builder(app, app.notifications.dataKey)
.setContentTitle(newNotificationsText)
.setContentText(buildSummaryText(summaryCounts))
.setTicker(newNotificationsText)
.setSmallIcon(R.drawable.ic_notification)
.setColor(0xff2196f3.toInt())
.setLights(0xff2196f3.toInt(), 2000, 2000)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setGroup(app.notifications.dataKey)
.setGroupSummary(true)
.setContentIntent(summaryIntent)
.setAutoCancel(true)
.build()
notificationManager.notify(app.notifications.dataId, summary)
}
}
}}
}

View File

@ -7,39 +7,39 @@ package pl.szczodrzynski.edziennik.data.api.task
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback
import pl.szczodrzynski.edziennik.data.api.szkolny.Szkolny
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.utils.Utils.d
class SzkolnyTask(val request: Any) : IApiTask(-1) {
class SzkolnyTask(val app: App, val syncingProfiles: List<Profile>) : IApiTask(-1) {
companion object {
private const val TAG = "SzkolnyTask"
fun sync(profiles: List<Profile>) = SzkolnyTask(SyncRequest(profiles))
/*fun shareEvent(event: EventFull) = SzkolnyTask(ShareEventRequest(event))
fun unshareEvent(event: EventFull) = SzkolnyTask(UnshareEventRequest(event))*/
}
private val api by lazy { SzkolnyApi(app) }
private val profiles by lazy { app.db.profileDao().allNow }
override fun prepare(app: App) { taskName = app.getString(R.string.edziennik_szkolny_creating_notifications) }
override fun cancel() {}
private lateinit var szkolny: Szkolny
private val notificationList = mutableListOf<Notification>()
override fun prepare(app: App) {
taskName = app.getString(R.string.edziennik_szkolny_api_sync_title)
internal fun run(taskCallback: EdziennikCallback) {
val startTime = System.currentTimeMillis()
val notifications = Notifications(app, notificationList, profiles)
// create all e-register data notifications
notifications.run()
// send notifications to web push, get shared events
AppSync(app, notificationList, profiles, api).run()
// create notifications for shared events (not present before app sync)
notifications.sharedEventNotifications()
d(TAG, "Created ${notificationList.count()} notifications.")
// update the database
app.db.metadataDao().setAllNotified(true)
app.db.notificationDao().addAll(notificationList)
app.db.profileDao().setAllNotEmpty()
// post all notifications
PostNotifications(app, notificationList)
d(TAG, "SzkolnyTask: finished in ${System.currentTimeMillis()-startTime} ms.")
taskCallback.onCompleted()
}
override fun cancel() {
// TODO
}
internal fun run(app: App, taskCallback: EdziennikCallback) {
szkolny = Szkolny(app, taskCallback)
when (request) {
is SyncRequest -> szkolny.sync(request.profiles)
/*is ShareEventRequest -> szkolny.shareEvent(request.event)
is UnshareEventRequest -> szkolny.unshareEvent(request.event)*/
}
}
data class SyncRequest(val profiles: List<Profile>)
/*data class ShareEventRequest(val event: EventFull)
data class UnshareEventRequest(val event: EventFull)*/
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,165 @@
package pl.szczodrzynski.edziennik.data.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import pl.szczodrzynski.edziennik.config.db.ConfigDao
import pl.szczodrzynski.edziennik.config.db.ConfigEntry
import pl.szczodrzynski.edziennik.data.db.converter.*
import pl.szczodrzynski.edziennik.data.db.dao.*
import pl.szczodrzynski.edziennik.data.db.entity.*
import pl.szczodrzynski.edziennik.data.db.migration.*
@Database(entities = [
Grade::class,
Teacher::class,
TeacherAbsence::class,
TeacherAbsenceType::class,
Subject::class,
Notice::class,
Team::class,
Attendance::class,
Event::class,
EventType::class,
LoginStore::class,
Profile::class,
LuckyNumber::class,
Announcement::class,
GradeCategory::class,
FeedbackMessage::class,
Message::class,
MessageRecipient::class,
DebugLog::class,
EndpointTimer::class,
LessonRange::class,
Notification::class,
Classroom::class,
NoticeType::class,
AttendanceType::class,
Lesson::class,
ConfigEntry::class,
LibrusLesson::class,
Metadata::class
], version = 76)
@TypeConverters(
ConverterTime::class,
ConverterDate::class,
ConverterJsonObject::class,
ConverterListLong::class,
ConverterListString::class,
ConverterDateInt::class
)
abstract class AppDb : RoomDatabase() {
abstract fun gradeDao(): GradeDao
abstract fun teacherDao(): TeacherDao
abstract fun teacherAbsenceDao(): TeacherAbsenceDao
abstract fun teacherAbsenceTypeDao(): TeacherAbsenceTypeDao
abstract fun subjectDao(): SubjectDao
abstract fun noticeDao(): NoticeDao
abstract fun teamDao(): TeamDao
abstract fun attendanceDao(): AttendanceDao
abstract fun eventDao(): EventDao
abstract fun eventTypeDao(): EventTypeDao
abstract fun loginStoreDao(): LoginStoreDao
abstract fun profileDao(): ProfileDao
abstract fun luckyNumberDao(): LuckyNumberDao
abstract fun announcementDao(): AnnouncementDao
abstract fun gradeCategoryDao(): GradeCategoryDao
abstract fun feedbackMessageDao(): FeedbackMessageDao
abstract fun messageDao(): MessageDao
abstract fun messageRecipientDao(): MessageRecipientDao
abstract fun debugLogDao(): DebugLogDao
abstract fun endpointTimerDao(): EndpointTimerDao
abstract fun lessonRangeDao(): LessonRangeDao
abstract fun notificationDao(): NotificationDao
abstract fun classroomDao(): ClassroomDao
abstract fun noticeTypeDao(): NoticeTypeDao
abstract fun attendanceTypeDao(): AttendanceTypeDao
abstract fun timetableDao(): TimetableDao
abstract fun configDao(): ConfigDao
abstract fun librusLessonDao(): LibrusLessonDao
abstract fun metadataDao(): MetadataDao
companion object {
@Volatile private var instance: AppDb? = null
private val LOCK = Any()
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
instance ?: buildDatabase(context).also { instance = it }
}
private fun buildDatabase(context: Context) = Room.databaseBuilder(
context.applicationContext,
AppDb::class.java,
"edziennik.db"
).addMigrations(
Migration12(),
Migration13(),
Migration14(),
Migration15(),
Migration16(),
Migration17(),
Migration18(),
Migration19(),
Migration20(),
Migration21(),
Migration22(),
Migration23(),
Migration24(),
Migration25(),
Migration26(),
Migration27(),
Migration28(),
Migration29(),
Migration30(),
Migration31(),
Migration32(),
Migration33(),
Migration34(),
Migration35(),
Migration36(),
Migration37(),
Migration38(),
Migration39(),
Migration40(),
Migration41(),
Migration42(),
Migration43(),
Migration44(),
Migration45(),
Migration46(),
Migration47(),
Migration48(),
Migration49(),
Migration50(),
Migration51(),
Migration52(),
Migration53(),
Migration54(),
Migration55(),
Migration56(),
Migration57(),
Migration58(),
Migration59(),
Migration60(),
Migration61(),
Migration62(),
Migration63(),
Migration64(),
Migration65(),
Migration66(),
Migration67(),
Migration68(),
Migration69(),
Migration70(),
Migration71(),
Migration72(),
Migration73(),
Migration74(),
Migration75(),
Migration76()
).allowMainThreadQueries().build()
}
}

View File

@ -16,8 +16,8 @@ import androidx.sqlite.db.SupportSQLiteQuery;
import java.util.List;
import pl.szczodrzynski.edziennik.data.db.entity.Announcement;
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull;
import pl.szczodrzynski.edziennik.data.db.entity.Metadata;
import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull;
import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ANNOUNCEMENT;
@ -69,4 +69,14 @@ public abstract class AnnouncementDao {
public List<AnnouncementFull> getNotNotifiedNow(int profileId) {
return getAllNow(profileId, "notified = 0");
}
@Query("SELECT " +
"*, " +
"teachers.teacherName || ' ' || teachers.teacherSurname AS teacherFullName " +
"FROM announcements " +
"LEFT JOIN teachers USING(profileId, teacherId) " +
"LEFT JOIN metadata ON announcementId = thingId AND thingType = "+TYPE_ANNOUNCEMENT+" AND metadata.profileId = announcements.profileId " +
"WHERE notified = 0 " +
"ORDER BY addedDate DESC")
public abstract List<AnnouncementFull> getNotNotifiedNow();
}

View File

@ -5,20 +5,20 @@
package pl.szczodrzynski.edziennik.data.db.dao;
import androidx.lifecycle.LiveData;
import androidx.sqlite.db.SimpleSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteQuery;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.RawQuery;
import androidx.sqlite.db.SimpleSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteQuery;
import java.util.List;
import pl.szczodrzynski.edziennik.data.db.entity.Attendance;
import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull;
import pl.szczodrzynski.edziennik.utils.models.Date;
import java.util.List;
import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ATTENDANCE;
@Dao
@ -72,6 +72,13 @@ public abstract class AttendanceDao {
return getAllNow(profileId, "notified = 0");
}
@Query("SELECT * FROM attendances " +
"LEFT JOIN subjects USING(profileId, subjectId) " +
"LEFT JOIN metadata ON attendanceId = thingId AND thingType = " + TYPE_ATTENDANCE + " AND metadata.profileId = attendances.profileId " +
"WHERE notified = 0 " +
"ORDER BY attendanceLessonDate DESC, attendanceStartTime DESC")
public abstract List<AttendanceFull> getNotNotifiedNow();
// only absent and absent_excused count as absences
// all the other types are counted as being present
@Query("SELECT \n" +

View File

@ -34,7 +34,7 @@ public abstract class EventDao {
public abstract long add(Event event);
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract void addAll(List<Event> eventList);
public abstract long[] addAll(List<Event> eventList);
@Query("DELETE FROM events WHERE profileId = :profileId")
public abstract void clear(int profileId);
@ -44,7 +44,7 @@ public abstract class EventDao {
@Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = :thingType AND thingId = :thingId")
public abstract void removeMetadata(int profileId, int thingType, long thingId);
@Transaction
public void remove(int profileId, int type, long id) {
public void remove(int profileId, long type, long id) {
remove(profileId, id);
removeMetadata(profileId, type == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, id);
}
@ -88,7 +88,7 @@ public abstract class EventDao {
public LiveData<List<EventFull>> getAllWhere(int profileId, String filter) {
return getAll(profileId, filter);
}
public LiveData<List<EventFull>> getAllByType(int profileId, int type, String filter) {
public LiveData<List<EventFull>> getAllByType(int profileId, long type, String filter) {
return getAll(profileId, "eventType = "+type+" AND "+filter);
}
public LiveData<List<EventFull>> getAllByDate(int profileId, @NonNull Date date) {
@ -125,6 +125,24 @@ public abstract class EventDao {
return getAllNow(profileId, "notified = 0");
}
@Query("SELECT eventId FROM events WHERE profileId = :profileId AND eventBlacklisted = 1")
public abstract List<Long> getBlacklistedIds(int profileId);
@Query("SELECT eventId FROM events WHERE eventBlacklisted = 1")
public abstract List<Long> getBlacklistedIds();
@Query("SELECT " +
"*, " +
"eventTypes.eventTypeName AS typeName, " +
"eventTypes.eventTypeColor AS typeColor " +
"FROM events " +
"LEFT JOIN subjects USING(profileId, subjectId) " +
"LEFT JOIN eventTypes USING(profileId, eventType) " +
"LEFT JOIN metadata ON eventId = thingId AND (thingType = " + TYPE_EVENT + " OR thingType = " + TYPE_HOMEWORK + ") AND metadata.profileId = events.profileId " +
"WHERE events.eventBlacklisted = 0 AND notified = 0 " +
"GROUP BY eventId " +
"ORDER BY addedDate ASC")
public abstract List<EventFull> getNotNotifiedNow();
public EventFull getByIdNow(int profileId, long eventId) {
List<EventFull> eventList = getAllNow(profileId, "eventId = "+eventId);
return eventList.size() == 0 ? null : eventList.get(0);
@ -146,13 +164,13 @@ public abstract class EventDao {
}
@Query("DELETE FROM events WHERE profileId = :profileId AND eventAddedManually = 0 AND eventDate >= :todayDate AND eventType = :type")
public abstract void removeFutureWithType(int profileId, Date todayDate, int type);
public abstract void removeFutureWithType(int profileId, Date todayDate, long type);
@Query("DELETE FROM events WHERE profileId = :profileId AND eventAddedManually = 0 AND eventDate >= :todayDate AND eventType != :exceptType")
public abstract void removeFutureExceptType(int profileId, Date todayDate, int exceptType);
public abstract void removeFutureExceptType(int profileId, Date todayDate, long exceptType);
@Transaction
public void removeFutureExceptTypes(int profileId, Date todayDate, List<Integer> exceptTypes) {
public void removeFutureExceptTypes(int profileId, Date todayDate, List<Long> exceptTypes) {
removeFuture(profileId, todayDate, "eventType NOT IN " + exceptTypes.toString().replace('[', '(').replace(']', ')'));
}

View File

@ -4,14 +4,14 @@
package pl.szczodrzynski.edziennik.data.db.dao;
import java.util.List;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import java.util.List;
import pl.szczodrzynski.edziennik.data.db.entity.EventType;
@Dao
@ -26,11 +26,14 @@ public interface EventTypeDao {
void clear(int profileId);
@Query("SELECT * FROM eventTypes WHERE profileId = :profileId AND eventType = :typeId")
EventType getByIdNow(int profileId, int typeId);
EventType getByIdNow(int profileId, long typeId);
@Query("SELECT * FROM eventTypes WHERE profileId = :profileId")
LiveData<List<EventType>> getAll(int profileId);
@Query("SELECT * FROM eventTypes WHERE profileId = :profileId")
List<EventType> getAllNow(int profileId);
@Query("SELECT * FROM eventTypes")
List<EventType> getAllNow();
}

View File

@ -1,40 +0,0 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-1-6
*/
package pl.szczodrzynski.edziennik.data.db.dao;
import java.util.List;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage;
import pl.szczodrzynski.edziennik.data.db.full.FeedbackMessageWithCount;
@Dao
public interface FeedbackMessageDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void add(FeedbackMessage feedbackMessage);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void addAll(List<FeedbackMessage> feedbackMessageList);
@Query("DELETE FROM feedbackMessages")
void clear();
@Query("SELECT * FROM feedbackMessages")
List<FeedbackMessage> getAllNow();
@Query("SELECT * FROM feedbackMessages WHERE fromUser = :fromUser")
List<FeedbackMessage> getAllByUserNow(String fromUser);
@Query("SELECT * FROM feedbackMessages")
LiveData<List<FeedbackMessage>> getAll();
@Query("SELECT *, COUNT(*) AS messageCount FROM feedbackMessages GROUP BY fromUser ORDER BY sentTime DESC")
List<FeedbackMessageWithCount> getAllWithCountNow();
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) Kacper Ziubryniewicz 2020-1-6
*/
package pl.szczodrzynski.edziennik.data.db.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage
@Dao
interface FeedbackMessageDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun add(feedbackMessage: FeedbackMessage)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun addAll(feedbackMessageList: List<FeedbackMessage>)
@Query("DELETE FROM feedbackMessages")
fun clear()
@get:Query("SELECT * FROM feedbackMessages ORDER BY sentTime DESC LIMIT 50")
val allNow: List<FeedbackMessage>
@Query("SELECT * FROM feedbackMessages WHERE deviceId = :deviceId ORDER BY sentTime DESC LIMIT 50")
fun getByDeviceIdNow(deviceId: String): List<FeedbackMessage>
@get:Query("SELECT * FROM feedbackMessages")
val all: LiveData<List<FeedbackMessage>>
@get:Query("SELECT *, COUNT(*) AS count FROM feedbackMessages WHERE received = 1 AND devId IS NULL AND deviceId != 'szkolny.eu' GROUP BY deviceId ORDER BY sentTime DESC")
val allWithCountNow: List<FeedbackMessage.WithCount>
}

View File

@ -83,6 +83,13 @@ public abstract class GradeDao {
return getAllNow(profileId, "gradeParentId = "+parentId);
}
@Query("SELECT * FROM grades " +
"LEFT JOIN subjects USING(profileId, subjectId) " +
"LEFT JOIN metadata ON gradeId = thingId AND thingType = " + TYPE_GRADE + " AND metadata.profileId = grades.profileId " +
"WHERE notified = 0 " +
"ORDER BY addedDate DESC")
public abstract List<GradeFull> getNotNotifiedNow();
@RawQuery
abstract GradeFull getNow(SupportSQLiteQuery query);
public GradeFull getNow(int profileId, String filter) {
@ -145,7 +152,7 @@ public abstract class GradeDao {
}
}
public LiveData<List<GradeFull>> getAllFromDate(int profileId, int semester, long date) {
return getAllWhere(profileId, "gradeSemester = " + semester + " AND addedDate > " + date);
public LiveData<List<GradeFull>> getAllFromDate(int profileId, long date) {
return getAllWhere(profileId, "addedDate > " + date);
}
}

View File

@ -20,4 +20,7 @@ interface LibrusLessonDao {
@Query("SELECT * FROM librusLessons WHERE profileId = :profileId")
fun getAllNow(profileId: Int): List<LibrusLesson>
@Query("DELETE FROM librusLessons WHERE profileId = :profileId")
fun clear(profileId: Int)
}

View File

@ -17,9 +17,9 @@ import androidx.sqlite.db.SupportSQLiteQuery;
import java.util.List;
import pl.szczodrzynski.edziennik.data.db.entity.LuckyNumber;
import pl.szczodrzynski.edziennik.data.db.full.LuckyNumberFull;
import pl.szczodrzynski.edziennik.data.db.entity.Metadata;
import pl.szczodrzynski.edziennik.data.db.entity.Notice;
import pl.szczodrzynski.edziennik.data.db.full.LuckyNumberFull;
import pl.szczodrzynski.edziennik.utils.models.Date;
import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_LUCKY_NUMBER;
@ -78,4 +78,10 @@ public abstract class LuckyNumberDao {
public List<LuckyNumberFull> getNotNotifiedNow(int profileId) {
return getAllNow(profileId, "notified = 0");
}
@Query("SELECT * FROM luckyNumbers\n" +
"LEFT JOIN metadata ON luckyNumberDate = thingId AND thingType = "+TYPE_LUCKY_NUMBER+" AND metadata.profileId = luckyNumbers.profileId " +
"WHERE notified = 0 " +
"ORDER BY addedDate DESC")
public abstract List<LuckyNumberFull> getNotNotifiedNow();
}

View File

@ -17,8 +17,8 @@ import androidx.sqlite.db.SupportSQLiteQuery;
import java.util.List;
import pl.szczodrzynski.edziennik.data.db.entity.Message;
import pl.szczodrzynski.edziennik.data.db.full.MessageFull;
import pl.szczodrzynski.edziennik.data.db.entity.Metadata;
import pl.szczodrzynski.edziennik.data.db.full.MessageFull;
import static pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_DELETED;
import static pl.szczodrzynski.edziennik.data.db.entity.Message.TYPE_RECEIVED;
@ -101,4 +101,14 @@ public abstract class MessageDao {
public List<MessageFull> getReceivedNotNotifiedNow(int profileId) {
return getReceivedNow(profileId, "notified = 0");
}
@Query("SELECT " +
"*, " +
"teachers.teacherName || ' ' || teachers.teacherSurname AS senderFullName " +
"FROM messages " +
"LEFT JOIN teachers ON teachers.profileId = messages.profileId AND teacherId = senderId " +
"LEFT JOIN metadata ON messageId = thingId AND thingType = "+TYPE_MESSAGE+" AND metadata.profileId = messages.profileId " +
"WHERE messageType = 0 AND notified = 0 " +
"ORDER BY addedDate DESC")
public abstract List<MessageFull> getReceivedNotNotifiedNow();
}

View File

@ -40,6 +40,9 @@ public abstract class MetadataDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
public abstract void addAllIgnore(List<Metadata> metadataList);
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract void addAllReplace(List<Metadata> metadataList);
@Query("UPDATE metadata SET seen = :seen WHERE thingId = :thingId AND thingType = :thingType AND profileId = :profileId")
abstract void updateSeen(int profileId, int thingType, long thingId, boolean seen);
@ -165,6 +168,9 @@ public abstract class MetadataDao {
@Query("UPDATE metadata SET notified = :notified WHERE profileId = :profileId")
public abstract void setAllNotified(int profileId, boolean notified);
@Query("UPDATE metadata SET notified = :notified")
public abstract void setAllNotified(boolean notified);
@Query("SELECT count() FROM metadata WHERE profileId = :profileId AND thingType = :thingType AND seen = 0")

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