activityContext;
+ private SyncCallback callback;
+ private Exception e = null;
+ private String apiResponse = null;
+ private int profileId;
+ private ProfileFull profile;
+
+ public ProcessAsyncTask(App app, Context activityContext, SyncCallback callback, int profileId, ProfileFull profile) {
+ //d(TAG, "Thread/ProcessAsyncTask/constructor/"+Thread.currentThread().getName());
+ this.app = app;
+ this.activityContext = new WeakReference<>(activityContext);
+ this.callback = callback;
+ this.profileId = profileId;
+ this.profile = profile;
+ }
+
+ @Override
+ protected Integer doInBackground(Void... voids) {
+ Context activityContext = this.activityContext.get();
+ //d(TAG, "Thread/ProcessAsyncTask/doInBackground/"+Thread.currentThread().getName());
+ try {
+
+ // UPDATE FCM TOKEN IF EMPTY
+ if (app.appConfig.fcmToken == null || app.appConfig.fcmToken.equals("")) {
+ FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(instanceIdResult -> {
+ app.appConfig.fcmToken = instanceIdResult.getToken();
+ app.appConfig.savePending = true;
+ });
+ }
+
+ callback.onProgress(1);
+ if (profile.getSyncNotifications()) {
+ new Handler(activityContext.getMainLooper()).post(() -> {
+ callback.onActionStarted(R.string.sync_action_creating_notifications);
+ });
+
+ for (LessonFull change : app.db.lessonChangeDao().getNotNotifiedNow(profileId)) {
+ String text = app.getContext().getString(R.string.notification_lesson_change_format, change.changeTypeStr(app.getContext()), change.lessonDate == null ? "" : change.lessonDate.getFormattedString(), change.subjectLongName);
+ app.notifier.add(new Notification(app.getContext(), text)
+ .withProfileData(profile.getId(), profile.getName())
+ .withType(Notification.TYPE_TIMETABLE_LESSON_CHANGE)
+ .withFragmentRedirect(MainActivity.DRAWER_ITEM_TIMETABLE)
+ .withLongExtra("timetableDate", change.lessonDate.getValue())
+ .withAddedDate(change.addedDate)
+ );
+ }
+ for (EventFull event : app.db.eventDao().getNotNotifiedNow(profileId)) {
+ String text;
+ if (event.type == TYPE_HOMEWORK)
+ text = app.getContext().getString(R.string.notification_homework_format, ns(app.getString(R.string.notification_event_no_subject), event.subjectLongName), event.eventDate.getFormattedString());
+ else
+ text = app.getContext().getString(R.string.notification_event_format, event.typeName, event.eventDate.getFormattedString(), ns(app.getString(R.string.notification_event_no_subject), event.subjectLongName));
+ app.notifier.add(new Notification(app.getContext(), text)
+ .withProfileData(profile.getId(), profile.getName())
+ .withType(event.type == TYPE_HOMEWORK ? Notification.TYPE_NEW_HOMEWORK : Notification.TYPE_NEW_EVENT)
+ .withFragmentRedirect(event.type == TYPE_HOMEWORK ? MainActivity.DRAWER_ITEM_HOMEWORKS : MainActivity.DRAWER_ITEM_AGENDA)
+ .withLongExtra("eventId", event.id)
+ .withLongExtra("eventDate", event.eventDate.getValue())
+ .withAddedDate(event.addedDate)
+ );
+ // student's rights abuse - disabled, because this was useless
+ /*if (!event.addedManually && event.type == RegisterEvent.TYPE_EXAM && event.eventDate.combineWith(event.startTime) - event.addedDate < 7 * 24 * 60 * 60 * 1000) {
+ text = app.getContext().getString(R.string.notification_abuse_format, event.typeString(app, app.profile), event.subjectLongName, event.eventDate.getFormattedString());
+ app.notifier.add(new Notification(app.getContext(), text)
+ .withProfileData(profile.id, profile.name)
+ .withType(Notification.TYPE_GENERAL)
+ .withFragmentRedirect(MainActivity.DRAWER_ITEM_NOTIFICATIONS)
+ );
+ }*/
+ }
+
+ Date today = Date.getToday();
+ int todayValue = today.getValue();
+ profile.setCurrentSemester(profile.dateToSemester(today));
+
+ for (GradeFull grade : app.db.gradeDao().getNotNotifiedNow(profileId)) {
+ String gradeName = grade.name;
+ if (grade.type == TYPE_SEMESTER1_PROPOSED
+ || grade.type == TYPE_SEMESTER2_PROPOSED) {
+ gradeName = (app.getString(R.string.grade_semester_proposed_format_2, grade.name));
+ } else if (grade.type == TYPE_SEMESTER1_FINAL
+ || grade.type == TYPE_SEMESTER2_FINAL) {
+ gradeName = (app.getString(R.string.grade_semester_final_format_2, grade.name));
+ } else if (grade.type == TYPE_YEAR_PROPOSED) {
+ gradeName = (app.getString(R.string.grade_year_proposed_format_2, grade.name));
+ } else if (grade.type == TYPE_YEAR_FINAL) {
+ gradeName = (app.getString(R.string.grade_year_final_format_2, grade.name));
+ }
+ String text = app.getContext().getString(R.string.notification_grade_format, gradeName, grade.subjectLongName);
+ app.notifier.add(new Notification(app.getContext(), text)
+ .withProfileData(profile.getId(), profile.getName())
+ .withType(Notification.TYPE_NEW_GRADE)
+ .withFragmentRedirect(MainActivity.DRAWER_ITEM_GRADES)
+ .withLongExtra("gradesSubjectId", grade.subjectId)
+ .withAddedDate(grade.addedDate)
+ );
+ }
+ for (NoticeFull notice : app.db.noticeDao().getNotNotifiedNow(profileId)) {
+ String noticeTypeStr = (notice.type == Notice.TYPE_POSITIVE ? app.getString(R.string.notification_notice_praise) : (notice.type == Notice.TYPE_NEGATIVE ? app.getString(R.string.notification_notice_warning) : app.getString(R.string.notification_notice_new)));
+ String text = app.getContext().getString(R.string.notification_notice_format, noticeTypeStr, notice.teacherFullName, Date.fromMillis(notice.addedDate).getFormattedString());
+ app.notifier.add(new Notification(app.getContext(), text)
+ .withProfileData(profile.getId(), profile.getName())
+ .withType(Notification.TYPE_NEW_NOTICE)
+ .withFragmentRedirect(MainActivity.DRAWER_ITEM_NOTICES)
+ .withLongExtra("noticeId", notice.id)
+ .withAddedDate(notice.addedDate)
+ );
+ }
+ for (AttendanceFull attendance : app.db.attendanceDao().getNotNotifiedNow(profileId)) {
+ String attendanceTypeStr = app.getString(R.string.notification_type_attendance);
+ switch (attendance.type) {
+ case Attendance.TYPE_ABSENT:
+ attendanceTypeStr = app.getString(R.string.notification_absence);
+ break;
+ case Attendance.TYPE_ABSENT_EXCUSED:
+ attendanceTypeStr = app.getString(R.string.notification_absence_excused);
+ break;
+ case Attendance.TYPE_BELATED:
+ attendanceTypeStr = app.getString(R.string.notification_belated);
+ break;
+ case Attendance.TYPE_BELATED_EXCUSED:
+ attendanceTypeStr = app.getString(R.string.notification_belated_excused);
+ break;
+ case Attendance.TYPE_RELEASED:
+ attendanceTypeStr = app.getString(R.string.notification_release);
+ break;
+ }
+ String text = app.getContext().getString(R.string.notification_attendance_format, attendanceTypeStr, attendance.subjectLongName, attendance.lessonDate.getFormattedString());
+ app.notifier.add(new Notification(app.getContext(), text)
+ .withProfileData(profile.getId(), profile.getName())
+ .withType(Notification.TYPE_NEW_ATTENDANCE)
+ .withFragmentRedirect(MainActivity.DRAWER_ITEM_ATTENDANCES)
+ .withLongExtra("attendanceId", attendance.id)
+ .withAddedDate(attendance.addedDate)
+ );
+ }
+ for (AnnouncementFull announcement : app.db.announcementDao().getNotNotifiedNow(profileId)) {
+ String text = app.getContext().getString(R.string.notification_announcement_format, announcement.subject);
+ app.notifier.add(new Notification(app.getContext(), text)
+ .withProfileData(profile.getId(), profile.getName())
+ .withType(Notification.TYPE_NEW_ANNOUNCEMENT)
+ .withFragmentRedirect(MainActivity.DRAWER_ITEM_ANNOUNCEMENTS)
+ .withLongExtra("announcementId", announcement.id)
+ .withAddedDate(announcement.addedDate)
+ );
+ }
+ for (MessageFull message : app.db.messageDao().getReceivedNotNotifiedNow(profileId)) {
+ String text = app.getContext().getString(R.string.notification_message_format, message.senderFullName, message.subject);
+ app.notifier.add(new Notification(app.getContext(), text)
+ .withProfileData(profile.getId(), profile.getName())
+ .withType(Notification.TYPE_NEW_MESSAGE)
+ .withFragmentRedirect(MainActivity.DRAWER_ITEM_MESSAGES)
+ .withLongExtra("messageType", Message.TYPE_RECEIVED)
+ .withLongExtra("messageId", message.id)
+ .withAddedDate(message.addedDate)
+ );
+ }
+
+ if (profile.getLuckyNumber() != oldLuckyNumber
+ && profile.getLuckyNumber() != -1
+ && profile.getLuckyNumberDate() != null
+ && profile.getLuckyNumberDate().getValue() >= todayValue) {
+ String text;
+ if (profile.getLuckyNumberDate().getValue() == todayValue) { // LN for today
+ text = app.getString((profile.getStudentNumber() != -1 && profile.getStudentNumber() == profile.getLuckyNumber() ? R.string.notification_lucky_number_yours_format : R.string.notification_lucky_number_format), profile.getLuckyNumber());
+ } else if (profile.getLuckyNumberDate().getValue() == todayValue + 1) { // LN for tomorrow
+ text = app.getString((profile.getStudentNumber() != -1 && profile.getStudentNumber() == profile.getLuckyNumber() ? R.string.notification_lucky_number_yours_tomorrow_format : R.string.notification_lucky_number_tomorrow_format), profile.getLuckyNumber());
+ } else { // LN for later
+ text = app.getString((profile.getStudentNumber() != -1 && profile.getStudentNumber() == profile.getLuckyNumber() ? R.string.notification_lucky_number_yours_later_format : R.string.notification_lucky_number_later_format), profile.getLuckyNumberDate().getFormattedString(), profile.getLuckyNumber());
+ }
+ app.notifier.add(new Notification(app.getContext(), text)
+ .withProfileData(profile.getId(), profile.getName())
+ .withType(Notification.TYPE_LUCKY_NUMBER)
+ .withFragmentRedirect(MainActivity.DRAWER_ITEM_HOME)
+ );
+ oldLuckyNumber = profile.getLuckyNumber();
+ }
+ }
+
+
+ app.db.metadataDao().setAllNotified(profileId, true);
+ callback.onProgress(1);
+
+ // SEND WEB PUSH, if registration allowed
+ // otherwise, UNREGISTER THE USER
+ if (profile.getRegistration() == REGISTRATION_ENABLED) {
+ new Handler(activityContext.getMainLooper()).post(() -> {
+ callback.onActionStarted(R.string.sync_action_syncing_shared_events);
+ });
+ //if (profile.registrationUsername == null || profile.registrationUsername.equals("")) {
+ //}
+ ServerRequest syncRequest = new ServerRequest(app, app.requestScheme + APP_URL + "main.php?sync", "Edziennik/REG", profile);
+
+ if (registerEmpty) {
+ syncRequest.setBodyParameter("first_run", "true");
+ }
+
+ // ALSO SEND NEW DATA TO BROWSER *excluding* all Shared Events !!!
+ // because they will be sent by the server, as soon as it's shared, by FCM
+
+ if (app.appConfig.webPushEnabled) {
+ int position = 0;
+ for (Notification notification : app.appConfig.notifications) {
+ //Log.d(TAG, notification.text);
+ if (!notification.notified) {
+ if (notification.type != Notification.TYPE_NEW_SHARED_EVENT
+ && notification.type != Notification.TYPE_SERVER_MESSAGE
+ && notification.type != Notification.TYPE_NEW_SHARED_HOMEWORK) // these are automatically sent to the browser by the server
+ {
+ //Log.d(TAG, "Adding notify[" + position + "]");
+ syncRequest.setBodyParameter("notify[" + position + "][type]", Integer.toString(notification.type));
+ syncRequest.setBodyParameter("notify[" + position + "][title]", notification.title);
+ syncRequest.setBodyParameter("notify[" + position + "][text]", notification.text);
+ position++;
+ }
+ }
+ }
+ }
+
+ callback.onProgress(1);
+
+ if (app.appConfig.webPushEnabled || profile.getEnableSharedEvents()) {
+ JsonObject result = syncRequest.runSync();
+ callback.onProgress(1);
+ //Log.d(TAG, "Executed request");
+ if (result == null) {
+ return AppError.CODE_APP_SERVER_ERROR;
+ }
+ apiResponse = result.toString();
+ if (!result.get("success").getAsString().equals("true")) {
+ return AppError.CODE_APP_SERVER_ERROR;
+ }
+ // HERE PROCESS ALL THE RECEIVED EVENTS
+ // add them to the profile and create appropriate notifications
+ for (JsonElement jEventEl : result.getAsJsonArray("events")) {
+ JsonObject jEvent = jEventEl.getAsJsonObject();
+ String teamCode = jEvent.get("team").getAsString();
+ //d(TAG, "An event is there! "+jEvent.toString());
+ // get the target Team from teamCode
+ Team team = app.db.teamDao().getByCodeNow(profile.getId(), teamCode);
+ if (team != null) {
+ //d(TAG, "The target team is "+team.name+", ID "+team.id);
+ // create the event from Json. Add the missing teamId and !!profileId!!
+ Event event = app.gson.fromJson(jEvent.toString(), Event.class);
+ // proguard. disable for Event.class
+ if (event.eventDate == null) {
+ apiResponse += "\n\nEventDate == null\n" + jEvent.toString();
+ throw new Exception("null eventDate");
+ }
+ event.profileId = profile.getId();
+ event.teamId = team.id;
+ event.addedManually = true;
+ //d(TAG, "Created the event! "+event);
+
+ if (event.sharedBy != null && event.sharedBy.equals(profile.getUsernameId())) {
+ //d(TAG, "Shared by self! Changing name");
+ event.sharedBy = "self";
+ event.sharedByName = profile.getStudentNameLong();
+ }
+
+ EventType type = app.db.eventTypeDao().getByIdNow(profileId, event.type);
+
+ //d(TAG, "Finishing adding event "+event);
+ app.db.eventDao().add(event);
+ Metadata metadata = new Metadata(profile.getId(), event.type == TYPE_HOMEWORK ? Metadata.TYPE_HOMEWORK : Metadata.TYPE_EVENT, event.id, registerEmpty, true, jEvent.get("addedDate").getAsLong());
+ long metadataId = app.db.metadataDao().add(metadata);
+ if (metadataId != -1 && !registerEmpty) {
+ app.notifier.add(new Notification(app.getContext(), app.getString(R.string.notification_shared_event_format, event.sharedByName, type != null ? type.name : "wydarzenie", event.eventDate == null ? "nieznana data" : event.eventDate.getFormattedString(), event.topic))
+ .withProfileData(profile.getId(), profile.getName())
+ .withType(event.type == TYPE_HOMEWORK ? Notification.TYPE_NEW_SHARED_HOMEWORK : Notification.TYPE_NEW_SHARED_EVENT)
+ .withFragmentRedirect(event.type == TYPE_HOMEWORK ? MainActivity.DRAWER_ITEM_HOMEWORKS : MainActivity.DRAWER_ITEM_AGENDA)
+ .withLongExtra("eventDate", event.eventDate.getValue())
+ );
+ }
+ }
+ }
+ callback.onProgress(5);
+ return CODE_OK;
+ } else {
+ callback.onProgress(6);
+ return CODE_OK;
+ }
+ } else {
+ // the user does not want to be registered
+ callback.onProgress(7);
+ return CODE_OK;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ this.e = e;
+ return null;
+ }
+ //return null;
+ }
+
+ @Override
+ protected void onPostExecute(Integer errorCode) {
+ //d(TAG, "Thread/ProcessAsyncTask/onPostExecute/"+Thread.currentThread().getName());
+ Context activityContext = this.activityContext.get();
+ app.profileSaveFull(profile);
+ if (app.profile != null && profile.getId() == app.profile.getId()) {
+ app.profile = profile;
+ }
+ if (errorCode == null) {
+ // this means an Exception was thrown
+ callback.onError(activityContext, new AppError(TAG, 513, CODE_OTHER, e, apiResponse));
+ return;
+ }
+ //Log.d(TAG, "Finishing");
+
+
+ callback.onProgress(1);
+
+ if (errorCode == CODE_OK)
+ callback.onSuccess(activityContext, profile);
+ else {
+ try {
+ // oh that's useless
+ throw new RuntimeException(stringErrorCode(app, errorCode, ""));
+ } catch (Exception e) {
+ callback.onError(activityContext, new AppError(TAG, 528, errorCode, e, (String) null));
+ }
+ }
+ super.onPostExecute(errorCode);
+ }
+ }
+
+ public void notifyAndReload() {
+ app.notifier.postAll(null);
+ app.saveConfig();
+ SyncJob.schedule(app);
+ Intent i = new Intent(Intent.ACTION_MAIN)
+ .putExtra("reloadProfileId", -1);
+ app.sendBroadcast(i);
+
+ Intent intent = new Intent(app.getContext(), WidgetTimetable.class);
+ intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
+ int[] ids = AppWidgetManager.getInstance(app).getAppWidgetIds(new ComponentName(app, WidgetTimetable.class));
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
+ app.sendBroadcast(intent);
+
+ intent = new Intent(app.getContext(), WidgetNotifications.class);
+ intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
+ ids = AppWidgetManager.getInstance(app).getAppWidgetIds(new ComponentName(app, WidgetNotifications.class));
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
+ app.sendBroadcast(intent);
+
+ intent = new Intent(app.getContext(), WidgetLuckyNumber.class);
+ intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
+ ids = AppWidgetManager.getInstance(app).getAppWidgetIds(new ComponentName(app, WidgetLuckyNumber.class));
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
+ app.sendBroadcast(intent);
+ }
+
+ /* _____
+ / ____|
+ | (___ _ _ _ __ ___
+ \___ \| | | | '_ \ / __|
+ ____) | |_| | | | | (__
+ |_____/ \__, |_| |_|\___|
+ __/ |
+ |__*/
+ // DataCallbacks that are *not* in Edziennik.sync need to be executed on the main thread.
+ // EdziennikInterface.sync is executed on a worker thread
+ // in Edziennik.sync/newCallback methods are called on a worker thread
+
+ // callback passed to Edziennik.sync is executed on the main thread
+ // thus, callback which is in guiSync is also on the main thread
+
+ /**
+ * Sync all Edziennik data.
+ * Used in services, login form and {@code guiSync}
+ *
+ * May be ran on worker thread.
+ * {@link EdziennikInterface}.sync is ran always on worker thread.
+ * Every callback is ran on the UI thread.
+ *
+ * @param app
+ * @param activityContext
+ * @param callback
+ * @param profileId
+ */
+ public void sync(@NonNull App app, @NonNull Context activityContext, @NonNull SyncCallback callback, int profileId) {
+ sync(app, activityContext, callback, profileId, (int[])null);
+ }
+ public void sync(@NonNull App app, @NonNull Context activityContext, @NonNull SyncCallback callback, int profileId, @Nullable int ... featureList) {
+ // empty: no unread notifications, all shared events (current+past)
+ // only if there is no data, and we are not logged in yet
+ SyncCallback newCallback = new SyncCallback() {
+ @Override
+ public void onLoginFirst(List profileList, LoginStore loginStore) {
+ new Handler(activityContext.getMainLooper()).post(() -> {
+ callback.onLoginFirst(profileList, loginStore);
+ });
+ }
+
+ @Override
+ public void onSuccess(Context activityContext, ProfileFull profileFull) {
+ new Handler(activityContext.getMainLooper()).post(() -> {
+ new ProcessAsyncTask(app, activityContext, callback, profileId, profileFull).execute();
+ });
+ }
+
+ @Override
+ public void onError(Context activityContext, AppError error) {
+ new Handler(activityContext.getMainLooper()).post(() -> {
+ callback.onError(activityContext, error);
+ });
+ }
+
+ @Override
+ public void onProgress(int progressStep) {
+ new Handler(activityContext.getMainLooper()).post(() -> {
+ callback.onProgress(progressStep);
+ });
+ }
+
+ @Override
+ public void onActionStarted(int stringResId) {
+ new Handler(activityContext.getMainLooper()).post(() -> {
+ callback.onActionStarted(stringResId);
+ });
+ }
+ };
+ AsyncTask.execute(() -> {
+ ProfileFull profile = app.db.profileDao().getByIdNow(profileId);
+ if (profile != null) {
+
+ if (profile.getArchived()) {
+ newCallback.onError(activityContext, new AppError(TAG, 678, CODE_PROFILE_ARCHIVED, profile.getName()));
+ return;
+ }
+ else if (profile.getDateYearEnd() != null && Date.getToday().getValue() >= profile.getDateYearEnd().getValue()) {
+ profile.setArchived(true);
+ app.notifier.add(new Notification(app.getContext(), app.getString(R.string.profile_auto_archiving_format, profile.getName(), profile.getDateYearEnd().getFormattedString()))
+ .withProfileData(profile.getId(), profile.getName())
+ .withType(Notification.TYPE_AUTO_ARCHIVING)
+ .withFragmentRedirect(DRAWER_ITEM_HOME)
+ .withLongExtra("autoArchiving", 1L)
+ );
+ app.notifier.postAll(null);
+ app.db.profileDao().add(profile);
+ if (App.profileId == profile.getId()) {
+ app.profile.setArchived(true);
+ }
+ newCallback.onSuccess(activityContext, profile);
+ return;
+ }
+
+ registerEmpty = profile.getEmpty();
+ oldLuckyNumber = profile.getLuckyNumber();
+ getApi(app, profile.getLoginStoreType()).syncFeature(activityContext, newCallback, profile, featureList);
+ } else {
+ new Handler(activityContext.getMainLooper()).post(() -> callback.onError(activityContext, new AppError(TAG, 609, CODE_PROFILE_NOT_FOUND, (String) null)));
+ }
+ });
+ }
+
+ /* _____ _ _ _____
+ / ____| | | |_ _|
+ | | __| | | | | | __ ___ __ __ _ _ __ _ __ ___ _ __ ___
+ | | |_ | | | | | | \ \ /\ / / '__/ _` | '_ \| '_ \ / _ \ '__/ __|
+ | |__| | |__| |_| |_ \ V V /| | | (_| | |_) | |_) | __/ | \__ \
+ \_____|\____/|_____| \_/\_/ |_| \__,_| .__/| .__/ \___|_| |___/
+ | | | |
+ |_| |*/
+ /**
+ * Sync all Edziennik data while showing a progress dialog.
+ * A wrapper for {@code sync}
+ *
+ * Does not switch between threads.
+ * All callbacks have to be executed on the UI thread.
+ *
+ * @param app an App singleton instance
+ * @param activity a parent activity
+ * @param profileId ID of the profile to sync
+ * @param dialogTitle a title of the dialog to show
+ * @param dialogText dialog's content
+ * @param successText a toast to show on success
+ */
+ public void guiSync(@NonNull App app, @NonNull Activity activity, int profileId, @StringRes int dialogTitle, @StringRes int dialogText, @StringRes int successText) {
+ guiSync(app, activity, profileId, dialogTitle, dialogText, successText, (int[])null);
+ }
+ public void guiSync(@NonNull App app, @NonNull Activity activity, int profileId, @StringRes int dialogTitle, @StringRes int dialogText, @StringRes int successText, int ... featureList) {
+ MaterialDialog progressDialog = new MaterialDialog.Builder(activity)
+ .title(dialogTitle)
+ .content(dialogText)
+ .progress(false, PROFILE_MAX_PROGRESS, false)
+ .canceledOnTouchOutside(false)
+ .show();
+ SyncCallback guiSyncCallback = new SyncCallback() {
+ @Override
+ public void onLoginFirst(List profileList, LoginStore loginStore) {
+
+ }
+
+ @Override
+ public void onSuccess(Context activityContext, ProfileFull profileFull) {
+ progressDialog.dismiss();
+ Toast.makeText(activityContext, successText, Toast.LENGTH_SHORT).show();
+ notifyAndReload();
+ // profiles are saved automatically, during app.saveConfig in processFinish
+ /*if (activityContext instanceof MainActivity) {
+ //((MainActivity) activityContext).reloadCurrentFragment("GuiSync");
+ ((MainActivity) activityContext).accountHeaderAddProfiles();
+ }*/
+ }
+
+ @Override
+ public void onError(Context activityContext, AppError error) {
+ progressDialog.dismiss();
+ guiShowErrorDialog((Activity) activityContext, error, R.string.sync_error_dialog_title);
+ }
+
+ @Override
+ public void onProgress(int progressStep) {
+ progressDialog.incrementProgress(progressStep);
+ }
+
+ @Override
+ public void onActionStarted(int stringResId) {
+ progressDialog.setContent(activity.getString(R.string.sync_action_format, activity.getString(stringResId)));
+ }
+ };
+ app.apiEdziennik.sync(app, activity, guiSyncCallback, profileId, featureList);
+ }
+ /**
+ * Sync all Edziennik data in background.
+ * A callback is executed on main thread.
+ * A wrapper for {@code sync}
+ *
+ * @param app an App singleton instance
+ * @param activity a parent activity
+ * @param profileId ID of the profile to sync
+ * @param syncCallback a callback
+ * @param feature a feature to sync
+ */
+ public void guiSyncSilent(@NonNull App app, @NonNull Activity activity, int profileId, SyncCallback syncCallback, int feature) {
+ SyncCallback guiSyncCallback = new SyncCallback() {
+ @Override
+ public void onLoginFirst(List profileList, LoginStore loginStore) {
+
+ }
+
+ @Override
+ public void onSuccess(Context activityContext, ProfileFull profileFull) {
+ notifyAndReload();
+ syncCallback.onSuccess(activityContext, profileFull);
+ }
+
+ @Override
+ public void onError(Context activityContext, AppError error) {
+ syncCallback.onError(activityContext, error);
+ }
+
+ @Override
+ public void onProgress(int progressStep) {
+ syncCallback.onProgress(progressStep);
+ }
+
+ @Override
+ public void onActionStarted(int stringResId) {
+ syncCallback.onActionStarted(stringResId);
+ }
+ };
+ app.apiEdziennik.sync(app, activity, guiSyncCallback, profileId, feature == FEATURE_ALL ? null : new int[]{feature});
+ }
+
+ /**
+ * Show a dialog allowing the user to choose which features to sync.
+ * Handles everything including pre-selecting the features basing on the current fragment.
+ *
+ * Will execute {@code sync} after the selection is made.
+ *
+ * A normal progress dialog is shown during the sync.
+ *
+ * @param app an App singleton instance
+ * @param activity a parent activity
+ * @param profileId ID of the profile to sync
+ * @param dialogTitle a title of the dialog to show
+ * @param dialogText dialog's content
+ * @param successText a toast to show on success
+ * @param currentFeature a feature id representing the currently opened fragment or caller
+ */
+ public void guiSyncFeature(@NonNull App app,
+ @NonNull Activity activity,
+ int profileId,
+ @StringRes int dialogTitle,
+ @StringRes int dialogText,
+ @StringRes int successText,
+ int currentFeature) {
+
+ String[] items = new String[]{
+ app.getString(R.string.menu_timetable),
+ app.getString(R.string.menu_agenda),
+ app.getString(R.string.menu_grades),
+ app.getString(R.string.menu_homework),
+ app.getString(R.string.menu_notices),
+ app.getString(R.string.menu_attendances),
+ app.getString(R.string.title_messages_inbox_single),
+ app.getString(R.string.title_messages_sent_single),
+ app.getString(R.string.menu_announcements)
+ };
+ int[] itemsIds = new int[]{
+ FEATURE_TIMETABLE,
+ FEATURE_AGENDA,
+ FEATURE_GRADES,
+ FEATURE_HOMEWORKS,
+ FEATURE_NOTICES,
+ FEATURE_ATTENDANCES,
+ FEATURE_MESSAGES_INBOX,
+ FEATURE_MESSAGES_OUTBOX,
+ FEATURE_ANNOUNCEMENTS
+ };
+ int[] selectedIndices;
+ if (currentFeature == FEATURE_ALL) {
+ selectedIndices = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8};
+ }
+ else {
+ selectedIndices = new int[]{Arrays.binarySearch(itemsIds, currentFeature)};
+ }
+
+ MaterialDialog dialog = new MaterialDialog.Builder(activity)
+ .title(R.string.sync_feature_title)
+ .content(R.string.sync_feature_text)
+ .positiveText(R.string.ok)
+ .negativeText(R.string.cancel)
+ .neutralText(R.string.sync_feature_all)
+ .items(items)
+ .itemsIds(itemsIds)
+ .itemsCallbackMultiChoice(ArrayUtils.toWrapperArray(selectedIndices), (dialog1, which, text) -> {
+ dialog1.getActionButton(DialogAction.POSITIVE).setEnabled(which.length > 0);
+ return true;
+ })
+ .alwaysCallMultiChoiceCallback()
+ .onPositive(((dialog1, which) -> {
+ List featureList = new ArrayList<>();
+ for (int i: dialog1.getSelectedIndices()) {
+ featureList.add(itemsIds[i]);
+ }
+ guiSync(app, activity, profileId, dialogTitle, dialogText, successText, ArrayUtils.toPrimitiveArray(featureList));
+ }))
+ .onNeutral(((dialog1, which) -> {
+ guiSync(app, activity, profileId, dialogTitle, dialogText, successText);
+ }))
+ .show();
+
+
+
+ }
+
+ public void guiShowArchivedDialog(Activity activity, String profileName) {
+ new MaterialDialog.Builder(activity)
+ .title(R.string.profile_archived_dialog_title)
+ .content(activity.getString(R.string.profile_archived_dialog_text_format, profileName))
+ .positiveText(R.string.ok)
+ .onPositive(((dialog, which) -> dialog.dismiss()))
+ .autoDismiss(false)
+ .show();
+ }
+
+ /* _____ _ _ _____
+ / ____| | | |_ _|
+ | | __| | | | | | ___ _ __ _ __ ___ _ __ ___
+ | | |_ | | | | | | / _ \ '__| '__/ _ \| '__/ __|
+ | |__| | |__| |_| |_ | __/ | | | | (_) | | \__ \
+ \_____|\____/|_____| \___|_| |_| \___/|_| |__*/
+ /**
+ * Used for reporting an exception somewhere in the code that is not part of Edziennik APIs.
+ *
+ * @param activity a parent activity
+ * @param errorLine the line of code where the error occurred
+ * @param e an Exception object
+ */
+ public void guiReportException(Activity activity, int errorLine, Exception e) {
+ guiReportError(activity, new AppError(TAG, errorLine, CODE_OTHER, "Błąd wewnętrzny aplikacji ("+errorLine+")", null, null, e, null), null);
+ }
+
+ public void guiShowErrorDialog(Activity activity, @NonNull AppError error, @StringRes int dialogTitle) {
+ if (error.errorCode == CODE_PROFILE_ARCHIVED) {
+ guiShowArchivedDialog(activity, error.errorText);
+ return;
+ }
+ error.changeIfCodeOther();
+ new MaterialDialog.Builder(activity)
+ .title(dialogTitle)
+ .content(error.asReadableString(activity))
+ .positiveText(R.string.ok)
+ .onPositive(((dialog, which) -> dialog.dismiss()))
+ .neutralText(R.string.sync_error_dialog_report_button)
+ .onNeutral(((dialog, which) -> {
+ guiReportError(activity, error, dialog);
+ }))
+ .autoDismiss(false)
+ .show();
+ }
+ public void guiShowErrorSnackbar(MainActivity activity, @NonNull AppError error) {
+ if (error.errorCode == CODE_PROFILE_ARCHIVED) {
+ guiShowArchivedDialog(activity, error.errorText);
+ return;
+ }
+
+ // TODO: 2019-08-28
+ IconicsDrawable icon = new IconicsDrawable(activity)
+ .icon(CommunityMaterial.Icon.cmd_alert_circle);
+ sizeDp(icon, 20);
+ colorInt(icon, Themes.INSTANCE.getPrimaryTextColor(activity));
+
+ error.changeIfCodeOther();
+ CafeBar.builder(activity)
+ .to(activity.findViewById(R.id.coordinator))
+ .content(error.asReadableString(activity))
+ .icon(icon)
+ .positiveText(R.string.more)
+ .positiveColor(0xff4caf50)
+ .negativeText(R.string.ok)
+ .negativeColor(0x66ffffff)
+ .onPositive((cafeBar -> guiReportError(activity, error, null)))
+ .onNegative((cafeBar -> cafeBar.dismiss()))
+ .autoDismiss(false)
+ .swipeToDismiss(true)
+ .floating(true)
+ .show();
+ }
+ public void guiReportError(Activity activity, AppError error, @Nullable MaterialDialog parentDialogToDisableNeutral) {
+ String errorDetails = error.getDetails(activity);
+ String htmlErrorDetails = ""+errorDetails+"";
+ htmlErrorDetails = htmlErrorDetails.replaceAll(activity.getPackageName(), ""+activity.getPackageName()+"");
+ htmlErrorDetails = htmlErrorDetails.replaceAll("\n", "
");
+
+ new MaterialDialog.Builder(activity)
+ .title(R.string.sync_report_dialog_title)
+ .content(Html.fromHtml(htmlErrorDetails))
+ .typeface(null, "RobotoMono-Regular.ttf")
+ .negativeText(R.string.close)
+ .onNegative(((dialog1, which1) -> dialog1.dismiss()))
+ .neutralText(R.string.copy_to_clipboard)
+ .onNeutral((dialog1, which1) -> {
+ ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(CLIPBOARD_SERVICE);
+ if (clipboard != null) {
+ ClipData clip = ClipData.newPlainText("Error report", errorDetails);
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(activity, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
+ }
+ })
+ .autoDismiss(false)
+ .positiveText(R.string.sync_report_dialog_button)
+ .checkBoxPromptRes(R.string.sync_report_dialog_include_api_response, true, null)
+ .onPositive(((dialog1, which1) -> AsyncTask.execute(() -> error.getApiResponse(activity, apiResponse -> {
+ new ServerRequest(app, app.requestScheme + APP_URL + "main.php?report", "Edziennik/Report")
+ .setBodyParameter("base64_encoded", Base64.encodeToString(errorDetails.getBytes(), Base64.DEFAULT))
+ .setBodyParameter("api_response", dialog1.isPromptCheckBoxChecked() ? Base64.encodeToString(apiResponse.getBytes(), Base64.DEFAULT) : "VW5jaGVja2Vk"/*Unchecked*/)
+ .run((e, result) -> {
+ new Handler(activity.getMainLooper()).post(() -> {
+ if (result != null)
+ {
+ if (result.get("success").getAsBoolean()) {
+ Toast.makeText(activity, activity.getString(R.string.crash_report_sent), Toast.LENGTH_SHORT).show();
+ dialog1.getActionButton(DialogAction.POSITIVE).setEnabled(false);
+ if (parentDialogToDisableNeutral != null)
+ parentDialogToDisableNeutral.getActionButton(DialogAction.NEUTRAL).setEnabled(false);
+ }
+ else {
+ Toast.makeText(activity, activity.getString(R.string.crash_report_cannot_send) + ": " + result.get("reason").getAsString(), Toast.LENGTH_LONG).show();
+ }
+ }
+ else
+ {
+ Toast.makeText(activity, activity.getString(R.string.crash_report_cannot_send)+" brak internetu", Toast.LENGTH_LONG).show();
+ }
+ });
+ });
+ }))))
+ .show();
+ }
+
+ /**
+ * A method that displays a dialog allowing the user to report an error that has occurred.
+ *
+ * @param activity a parent activity
+ * @param errorCode self-explanatory
+ * @param errorText additional error information, that replaces text based on {@code errorCode} if it's {@code CODE_OTHER}
+ * @param throwable a {@link Throwable} containing the error details
+ * @param apiResponse response of the Edziennik API
+ * @param parentDialogToDisableNeutral if not null, an instance of {@link MaterialDialog} in which the neutral button should be disabled after submitting an error report
+ */
+ public void guiReportError(Activity activity, int errorCode, String errorText, Throwable throwable, String apiResponse, @Nullable MaterialDialog parentDialogToDisableNeutral) {
+ // build a string containing the stack trace and the device name + user's registration data
+ String contentPlain = "Application Internal Error "+stringErrorType(errorCode)+":\n"+stringErrorCode(activity, errorCode, "")+"\n"+errorText+"\n\n";
+ contentPlain += Log.getStackTraceString(throwable);
+ String content = ""+contentPlain+"";
+ content = content.replaceAll(activity.getPackageName(), ""+activity.getPackageName()+"");
+ content = content.replaceAll("\n", "
");
+
+ contentPlain += "\n"+Build.MANUFACTURER+"\n"+Build.BRAND+"\n"+Build.MODEL+"\n"+Build.DEVICE+"\n";
+ if (app.profile != null && app.profile.getRegistration() == REGISTRATION_ENABLED) {
+ contentPlain += "U: "+app.profile.getUsernameId()+"\nS: "+ app.profile.getStudentNameLong() +"\nT: "+app.profile.loginStoreType()+"\n";
+ }
+ contentPlain += BuildConfig.VERSION_NAME+" "+BuildConfig.BUILD_TYPE+"\nAndroid "+Build.VERSION.RELEASE;
+
+ d(TAG, contentPlain);
+ d(TAG, apiResponse == null ? "API Response = null" : apiResponse);
+
+
+ // show a dialog containing the error details in HTML
+ String finalContentPlain = contentPlain;
+ new MaterialDialog.Builder(activity)
+ .title(R.string.sync_report_dialog_title)
+ .content(Html.fromHtml(content))
+ .typeface(null, "RobotoMono-Regular.ttf")
+ .negativeText(R.string.close)
+ .onNegative(((dialog1, which1) -> dialog1.dismiss()))
+ .neutralText(R.string.copy_to_clipboard)
+ .onNeutral((dialog1, which1) -> {
+ ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(CLIPBOARD_SERVICE);
+ if (clipboard != null) {
+ ClipData clip = ClipData.newPlainText("Error report", finalContentPlain);
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(activity, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
+ }
+ })
+ .autoDismiss(false)
+ .positiveText(R.string.sync_report_dialog_button)
+ .checkBoxPromptRes(R.string.sync_report_dialog_include_api_response, true, null)
+ .onPositive(((dialog1, which1) -> {
+ // send the error report
+ new ServerRequest(app, app.requestScheme + APP_URL + "main.php?report", "Edziennik/Report")
+ .setBodyParameter("base64_encoded", Base64.encodeToString(finalContentPlain.getBytes(), Base64.DEFAULT))
+ .setBodyParameter("api_response", dialog1.isPromptCheckBoxChecked() ? apiResponse == null ? Base64.encodeToString("NULL XD".getBytes(), Base64.DEFAULT) : Base64.encodeToString(apiResponse.getBytes(), Base64.DEFAULT) : "VW5jaGVja2Vk"/*Unchecked*/)
+ .run((e, result) -> {
+ new Handler(Looper.getMainLooper()).post(() -> {
+ if (result != null)
+ {
+ if (result.get("success").getAsBoolean()) {
+ Toast.makeText(activity, activity.getString(R.string.crash_report_sent), Toast.LENGTH_SHORT).show();
+ dialog1.getActionButton(DialogAction.POSITIVE).setEnabled(false);
+ if (parentDialogToDisableNeutral != null)
+ parentDialogToDisableNeutral.getActionButton(DialogAction.NEUTRAL).setEnabled(false);
+ }
+ else {
+ Toast.makeText(activity, activity.getString(R.string.crash_report_cannot_send) + ": " + result.get("reason").getAsString(), Toast.LENGTH_LONG).show();
+ }
+ }
+ else
+ {
+ Toast.makeText(activity, activity.getString(R.string.crash_report_cannot_send)+" JsonObject equals null", Toast.LENGTH_LONG).show();
+ }
+ });
+ });
+ }))
+ .show();
+ }
+
+ /* _____ __ _ _ _
+ | __ \ / _(_) | | |
+ | |__) | __ ___ | |_ _| | ___ _ __ ___ _ __ ___ _____ ____ _| |
+ | ___/ '__/ _ \| _| | |/ _ \ | '__/ _ \ '_ ` _ \ / _ \ \ / / _` | |
+ | | | | | (_) | | | | | __/ | | | __/ | | | | | (_) \ V / (_| | |
+ |_| |_| \___/|_| |_|_|\___| |_| \___|_| |_| |_|\___/ \_/ \__,_|*/
+ public void guiRemoveProfile(MainActivity activity, int profileId, String profileName) {
+ new MaterialDialog.Builder(activity)
+ .title(R.string.profile_menu_remove_confirm)
+ .content(activity.getString(R.string.profile_menu_remove_confirm_text_format, profileName, profileName))
+ .positiveText(R.string.remove)
+ .negativeText(R.string.cancel)
+ .onPositive(((dialog, which) -> {
+ AsyncTask.execute(() -> {
+ removeProfile(profileId);
+ activity.runOnUiThread(() -> {
+ //activity.drawer.loadItem(DRAWER_ITEM_HOME, null, "ProfileRemoving");
+ //activity.recreate(DRAWER_ITEM_HOME);
+ activity.reloadTarget();
+ Toast.makeText(activity, "Profil został usunięty.", Toast.LENGTH_LONG).show();
+ });
+ });
+ }))
+ .show();
+ }
+ public void removeProfile(int profileId) {
+ Profile profileObject = app.db.profileDao().getByIdNow(profileId);
+ if (profileObject == null)
+ return;
+ app.db.announcementDao().clear(profileId);
+ app.db.attendanceDao().clear(profileId);
+ app.db.eventDao().clear(profileId);
+ app.db.eventTypeDao().clear(profileId);
+ app.db.gradeDao().clear(profileId);
+ app.db.gradeCategoryDao().clear(profileId);
+ app.db.lessonDao().clear(profileId);
+ app.db.lessonChangeDao().clear(profileId);
+ app.db.luckyNumberDao().clear(profileId);
+ app.db.noticeDao().clear(profileId);
+ app.db.subjectDao().clear(profileId);
+ app.db.teacherDao().clear(profileId);
+ app.db.teamDao().clear(profileId);
+ app.db.messageRecipientDao().clear(profileId);
+ app.db.messageDao().clear(profileId);
+
+ int loginStoreId = profileObject.getLoginStoreId();
+ List profilesUsingLoginStore = app.db.profileDao().getIdsByLoginStoreIdNow(loginStoreId);
+ if (profilesUsingLoginStore.size() == 1) {
+ app.db.loginStoreDao().remove(loginStoreId);
+ }
+ app.db.profileDao().remove(profileId);
+ app.db.metadataDao().deleteAll(profileId);
+
+ List