diff --git a/app/src/main/java/com/noah/timely/about/TimelyBasicInfoDialog.java b/app/src/main/java/com/noah/timely/about/TimelyBasicInfoDialog.java
index f393ca97..69a66a35 100644
--- a/app/src/main/java/com/noah/timely/about/TimelyBasicInfoDialog.java
+++ b/app/src/main/java/com/noah/timely/about/TimelyBasicInfoDialog.java
@@ -1,9 +1,11 @@
package com.noah.timely.about;
+import static com.noah.timely.util.AppInfoUtils.getAppVesionName;
+
import android.app.Dialog;
import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
+import android.content.Intent;
+import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
@@ -16,7 +18,6 @@
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
-import com.noah.timely.BuildConfig;
import com.noah.timely.R;
public class TimelyBasicInfoDialog extends DialogFragment implements View.OnClickListener {
@@ -55,18 +56,13 @@ protected void onCreate(Bundle savedInstanceState) {
ImageButton btn_close = findViewById(R.id.close);
btn_close.setOnClickListener(TimelyBasicInfoDialog.this);
- TextView tv_version = findViewById(R.id.version);
-
- String version = BuildConfig.VERSION_NAME;
- String packageName = "com.noah.timely";
+ findViewById(R.id.bmc).setOnClickListener(v -> {
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.buymeacoffee.com/noahweasley"));
+ getActivity().startActivity(Intent.createChooser(intent, getString(R.string.link_open_text)));
+ });
- try {
- PackageInfo packageInfo = getContext().getPackageManager().getPackageInfo(packageName, 0);
- version = packageInfo.versionName;
- } catch (PackageManager.NameNotFoundException ignored) {
- }
-
- tv_version.setText(String.format("V%s", version));
+ TextView tv_version = findViewById(R.id.version);
+ tv_version.setText(String.format("V%s", getAppVesionName(getContext())));
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/noah/timely/alarms/AlarmActivity.java b/app/src/main/java/com/noah/timely/alarms/AlarmActivity.java
index 12c7b482..a333d527 100644
--- a/app/src/main/java/com/noah/timely/alarms/AlarmActivity.java
+++ b/app/src/main/java/com/noah/timely/alarms/AlarmActivity.java
@@ -7,9 +7,11 @@
import android.content.Intent;
import android.content.SharedPreferences;
+import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
+import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
@@ -31,9 +33,19 @@ public class AlarmActivity extends AppCompatActivity {
private Intent receiverDismiss;
@Override
+ @SuppressWarnings("deprecation")
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.alarm_view);
+ // keep screen on
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ // show activity even when device is locked
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ setShowWhenLocked(true);
+ } else {
+ // noinspection deprecation
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ }
Intent starterIntent = getIntent();
// Register this activity as a receiver of the MessageEvent posts
diff --git a/app/src/main/java/com/noah/timely/alarms/AlarmModel.java b/app/src/main/java/com/noah/timely/alarms/AlarmModel.java
index f0969fff..8327752f 100644
--- a/app/src/main/java/com/noah/timely/alarms/AlarmModel.java
+++ b/app/src/main/java/com/noah/timely/alarms/AlarmModel.java
@@ -4,6 +4,8 @@
import com.noah.timely.core.DataModel;
+import java.util.Arrays;
+
@SuppressWarnings("unused")
public class AlarmModel extends DataModel {
private String time;
@@ -137,4 +139,22 @@ public int getPosition() {
public void setPosition(int position) {
this.position = position;
}
+
+ @Override
+ @SuppressWarnings("all")
+ public String toString() {
+ return "AlarmModel{" +
+ "time='" + time + '\'' +
+ ", isOn=" + isOn +
+ ", isRepeated=" + isRepeated +
+ ", ringTone='" + ringTone + '\'' +
+ ", repeatDays=" + Arrays.toString(repeatDays) +
+ ", position=" + position +
+ ", vibrate=" + vibrate +
+ ", label='" + label + '\'' +
+ ", initialPosition=" + initialPosition +
+ ", snoozed=" + snoozed +
+ ", snoozedTime='" + snoozedTime + '\'' +
+ '}';
+ }
}
diff --git a/app/src/main/java/com/noah/timely/alarms/AlarmNotificationService.java b/app/src/main/java/com/noah/timely/alarms/AlarmNotificationService.java
index 80ddf892..ebe51b85 100644
--- a/app/src/main/java/com/noah/timely/alarms/AlarmNotificationService.java
+++ b/app/src/main/java/com/noah/timely/alarms/AlarmNotificationService.java
@@ -20,6 +20,7 @@
import android.os.Process;
import android.os.VibrationEffect;
import android.os.Vibrator;
+import android.os.VibratorManager;
import android.text.TextUtils;
import androidx.preference.PreferenceManager;
@@ -73,7 +74,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
.build();
String type = PreferenceManager.getDefaultSharedPreferences(aCtxt)
- .getString("Alarm Ringtone", "TimeLY's Default");
+ .getString("Alarm Ringtone", "TimeLY's Default");
final Uri DEFAULT_URI = type.equals("TimeLY's Default") || SYSTEM_DEFAULT == null ? APP_DEFAULT
: SYSTEM_DEFAULT;
@@ -93,11 +94,19 @@ public int onStartCommand(Intent intent, int flags, int startId) {
}
- vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ // retrive the default vibrator
+ VibratorManager vManager = (VibratorManager) getSystemService(Context.VIBRATOR_MANAGER_SERVICE);
+ vibrator = vManager.getDefaultVibrator();
+ } else {
+ // noinspection deprecation
+ vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
+ }
+
if (isAlarmVibrate) {
final int DELAY = 0, VIBRATE = 1000, SLEEP = 1000, START = 0;
- long[] vibratePattern = {DELAY, VIBRATE, SLEEP};
+ long[] vibratePattern = { DELAY, VIBRATE, SLEEP };
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createWaveform(vibratePattern, START));
@@ -157,14 +166,14 @@ public void run() {
String ss = preferences.getString("snoozeOnStop", "Snooze");
if (ss.equals("Snooze")) sendBroadcast(new Intent(this, NotificationActionReceiver.class)
- .putExtra("action", "Snooze")
- .putExtra(ID, NOTIFICATION_ID)
- .putExtra(ALARM_POS, alarmPos));
+ .putExtra("action", "Snooze")
+ .putExtra(ID, NOTIFICATION_ID)
+ .putExtra(ALARM_POS, alarmPos));
else sendBroadcast(new Intent(this, NotificationActionReceiver.class)
- .putExtra("action", "Dismiss")
- .putExtra(ID, NOTIFICATION_ID)
- .putExtra(ALARM_POS, alarmPos));
+ .putExtra("action", "Dismiss")
+ .putExtra(ID, NOTIFICATION_ID)
+ .putExtra(ALARM_POS, alarmPos));
stopSelf();
}
diff --git a/app/src/main/java/com/noah/timely/alarms/AlarmReScheduler.java b/app/src/main/java/com/noah/timely/alarms/AlarmReScheduler.java
index 95918e91..95ae728a 100644
--- a/app/src/main/java/com/noah/timely/alarms/AlarmReScheduler.java
+++ b/app/src/main/java/com/noah/timely/alarms/AlarmReScheduler.java
@@ -4,6 +4,8 @@
import android.content.Context;
import android.content.Intent;
+import com.noah.timely.util.Constants;
+
/**
* An alarm re-scheduler that re-schedules alarm from TimeLY's database.
*
@@ -23,7 +25,9 @@ public void onReceive(Context context, Intent intent) {
|| /* htc devices */ action.equals("com.htc.intent.action.QUICKBOOT_POWERON");
if (isValidBootAction) {
- context.startService(new Intent(context, AlarmReSchedulerService.class));
+ Intent serviceIntent = new Intent(context, AlarmReSchedulerService.class);
+ serviceIntent.setAction(Constants.ACTION.SHOW_NOTIFICATION);
+ context.startService(serviceIntent);
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/noah/timely/alarms/AlarmReSchedulerService.java b/app/src/main/java/com/noah/timely/alarms/AlarmReSchedulerService.java
index 28c48539..790e5d43 100644
--- a/app/src/main/java/com/noah/timely/alarms/AlarmReSchedulerService.java
+++ b/app/src/main/java/com/noah/timely/alarms/AlarmReSchedulerService.java
@@ -12,16 +12,24 @@
import static com.noah.timely.timetable.DaysFragment.ARG_POSITION;
import android.app.AlarmManager;
+import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+import androidx.core.content.ContextCompat;
+import androidx.preference.PreferenceManager;
import com.noah.timely.R;
import com.noah.timely.assignment.AssignmentModel;
@@ -29,11 +37,13 @@
import com.noah.timely.assignment.SubmissionNotifier;
import com.noah.timely.core.DataModel;
import com.noah.timely.core.SchoolDatabase;
+import com.noah.timely.main.App;
import com.noah.timely.scheduled.AddScheduledDialog;
import com.noah.timely.scheduled.ScheduledTaskNotifier;
import com.noah.timely.timetable.DaysFragment;
import com.noah.timely.timetable.TimetableModel;
import com.noah.timely.timetable.TimetableNotifier;
+import com.noah.timely.util.Constants;
import com.noah.timely.util.ThreadUtils;
import java.util.Calendar;
@@ -45,15 +55,18 @@
* Service that would re-schedule all TimeLY alarms
*/
public class AlarmReSchedulerService extends Service {
+ private static final int NOTIFICATION_ID = 234543; // a very random number that came out of nowhere
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
+ Context context = getApplicationContext();
+ if (intent.getAction().equals(Constants.ACTION.SHOW_NOTIFICATION)) {
+ showActivityNofication(context);
+ }
+
// make re-scheduler return immediately to avoid blocking main thread
ThreadUtils.runBackgroundTask(() -> {
-
- Context context = getApplicationContext();
SchoolDatabase database = new SchoolDatabase(context);
-
// Reset the alarm here.
List activeAlarms = database.getActiveAlarms();
if (!activeAlarms.isEmpty()) {
@@ -105,11 +118,48 @@ public int onStartCommand(Intent intent, int flags, int startId) {
}
}
+ // cancel on-going notification
+ cancelNotification(context);
}); // end re-schedule task
return START_STICKY;
}
+ private void cancelNotification(Context context) {
+ NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ manager.cancel(NOTIFICATION_ID);
+ }
+
+ private void showActivityNofication(Context context) {
+ Uri SYSTEM_DEFAULT = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
+ Uri APP_DEFAULT = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+ .authority(context.getPackageName())
+ .path(String.valueOf(R.raw.arpeggio1))
+ .build();
+
+ String type = PreferenceManager.getDefaultSharedPreferences(context)
+ .getString("Uri Type", "TimeLY's Default");
+
+ final Uri DEFAULT_URI = type.equals("TimeLY's Default") || SYSTEM_DEFAULT == null ? APP_DEFAULT
+ : SYSTEM_DEFAULT;
+
+ Bitmap icon = BitmapFactory.decodeResource(context.getResources(), R.mipmap.app_icon);
+ NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, App.GENERAL_CHANNEL_ID);
+
+ builder.setContentTitle(getString(R.string.app_name) + " is in the background")
+ .setContentText(getString(R.string.app_name) + " is scheduling new notifications")
+ .setChannelId(App.GENERAL_CHANNEL_ID)
+ .setSound(DEFAULT_URI)
+ .setSmallIcon(R.drawable.ic_baseline_info_24)
+ .setColor(ContextCompat.getColor(context, R.color.colorPrimary))
+ .setLargeIcon(icon)
+ .setOngoing(true);
+
+ manager.notify(NOTIFICATION_ID, builder.build());
+ }
+
@Nullable
@Override
public IBinder onBind(Intent intent) {
@@ -144,7 +194,7 @@ private void registerPendingScheduledTimetables(Context context, TimetableModel
.addCategory("com.noah.timely.scheduled")
.setAction("com.noah.timely.scheduled.addAction")
.setDataAndType(Uri.parse("content://com.noah.timely.scheduled.add." + triggerTime),
- "com.noah.timely.scheduled.dataType");
+ "com.noah.timely.scheduled.dataType");
PendingIntent pi = PendingIntent.getBroadcast(context, 1156, scheduleIntent, 0);
@@ -183,14 +233,14 @@ private void registerPendingTimetables(Context context, TimetableModel timetable
Intent timetableIntent = new Intent(context, TimetableNotifier.class);
timetableIntent.putExtra(DaysFragment.ARG_TIME, time)
- .putExtra(ARG_CLASS, course)
- .putExtra(ARG_DAY, timetable.getCalendarDay())
- .putExtra(ARG_POSITION, position)
- .putExtra(ARG_PAGE_POSITION, position)
- .addCategory("com.noah.timely.timetable")
- .setAction("com.noah.timely.timetable.addAction")
- .setDataAndType(Uri.parse("content://com.noah.timely.add." + timeInMillis),
- "com.noah.timely.dataType");
+ .putExtra(ARG_CLASS, course)
+ .putExtra(ARG_DAY, timetable.getCalendarDay())
+ .putExtra(ARG_POSITION, position)
+ .putExtra(ARG_PAGE_POSITION, position)
+ .addCategory("com.noah.timely.timetable")
+ .setAction("com.noah.timely.timetable.addAction")
+ .setDataAndType(Uri.parse("content://com.noah.timely.add." + timeInMillis),
+ "com.noah.timely.dataType");
PendingIntent pi = PendingIntent.getBroadcast(context, 555, timetableIntent, PendingIntent.FLAG_UPDATE_CURRENT);
@@ -231,26 +281,26 @@ private void registerPendingAssignments(Context context, AssignmentModel assignm
String ln = truncateLecturerName(context, assignment.getLecturerName());
Intent notifyIntentCurrent = new Intent(context, SubmissionNotifier.class);
notifyIntentCurrent.putExtra(LECTURER_NAME, ln)
- .putExtra(TITLE, assignment.getTitle())
- .putExtra(POSITION, assignment.getPosition())
- .addCategory(context.getPackageName() + ".category")
- .setAction(context.getPackageName() + ".update")
- .setDataAndType(Uri.parse("content://" + context.getPackageName()),
- assignment.toString());
+ .putExtra(TITLE, assignment.getTitle())
+ .putExtra(POSITION, assignment.getPosition())
+ .addCategory(context.getPackageName() + ".category")
+ .setAction(context.getPackageName() + ".update")
+ .setDataAndType(Uri.parse("content://" + context.getPackageName()),
+ assignment.toString());
Intent notifyIntentPrevious = new Intent(context, Reminder.class);
notifyIntentPrevious.putExtra(LECTURER_NAME, ln)
- .putExtra(TITLE, assignment.getTitle())
- .putExtra(NEXT_ALARM, CURRENT)
- .addCategory(context.getPackageName() + ".category")
- .setAction(context.getPackageName() + ".update")
- .setDataAndType(Uri.parse("content://" + context.getPackageName()),
- assignment.toString());
+ .putExtra(TITLE, assignment.getTitle())
+ .putExtra(NEXT_ALARM, CURRENT)
+ .addCategory(context.getPackageName() + ".category")
+ .setAction(context.getPackageName() + ".update")
+ .setDataAndType(Uri.parse("content://" + context.getPackageName()),
+ assignment.toString());
PendingIntent assignmentPiPrevious = PendingIntent.getBroadcast(context, 147, notifyIntentPrevious,
- PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent assignmentPiCurrent = PendingIntent.getBroadcast(context, 141, notifyIntentCurrent,
- PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent.FLAG_UPDATE_CURRENT);
// Exact alarms not used here, so that android can perform its normal operation
// on devices
// >= 4.4 (KITKAT) to prevent unnecessary battery drain by alarms.
@@ -272,8 +322,8 @@ private void registerPendingAssignments(Context context, AssignmentModel assignm
private String truncateLecturerName(Context context, String fullName) {
String[] nameTokens = fullName.split(" ");
- String[] titles = {"Barr", "Barrister", "Doc", "Doctor", "Dr", "Engineer", "Engr",
- "Mr", "Mister", "Mrs", "Ms", "Prof", "Professor"};
+ String[] titles = { "Barr", "Barrister", "Doc", "Doctor", "Dr", "Engineer", "Engr",
+ "Mr", "Mister", "Mrs", "Ms", "Prof", "Professor" };
StringBuilder nameBuilder = new StringBuilder();
String shortenedName = "";
@@ -374,11 +424,11 @@ private void rescheduleNonRepeatingAlarm(@NonNull Context context, @NonNull Alar
alarmReceiverIntent.addCategory("com.noah.timely.alarm.category");
alarmReceiverIntent.setAction("com.noah.timely.alarm.cancel");
alarmReceiverIntent.setDataAndType(Uri.parse("content://com.noah.timely/Alarms/alarm" + alarmMillis),
- "com.noah.timely.alarm.dataType");
+ "com.noah.timely.alarm.dataType");
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent alarmPI = PendingIntent.getBroadcast(context, 11789, alarmReceiverIntent,
- PendingIntent.FLAG_CANCEL_CURRENT);
+ PendingIntent.FLAG_CANCEL_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// alarm has to be triggered even when device is in idle or doze mode.
// This alarm is very important
@@ -422,11 +472,11 @@ private void rescheduleRepeatingPendingAlarm(@NonNull Context context, @NonNull
alarmReceiverIntent.addCategory("com.noah.timely.alarm.category");
alarmReceiverIntent.setAction("com.noah.timely.alarm.cancel");
alarmReceiverIntent.setDataAndType(Uri.parse("content://com.noah.timely/Alarms/alarm" + alarmMillis),
- "com.noah.timely.alarm.dataType");
+ "com.noah.timely.alarm.dataType");
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent alarmPI = PendingIntent.getBroadcast(context, 11789, alarmReceiverIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
diff --git a/app/src/main/java/com/noah/timely/alarms/AlarmReceiver.java b/app/src/main/java/com/noah/timely/alarms/AlarmReceiver.java
index 328b56a1..b2d4d8d9 100644
--- a/app/src/main/java/com/noah/timely/alarms/AlarmReceiver.java
+++ b/app/src/main/java/com/noah/timely/alarms/AlarmReceiver.java
@@ -21,10 +21,10 @@
import java.util.Locale;
public class AlarmReceiver extends BroadcastReceiver {
+ public static String ALARM_POS = "com.noah.timely.position";
static final int NOTIFICATION_ID = 11789;
static final String ID = "Notification ID";
static final String REPEAT_DAYS = "Alarm Repeat Days";
- public static String ALARM_POS = "com.noah.timely.position";
@Override
public void onReceive(Context context, Intent intent) {
diff --git a/app/src/main/java/com/noah/timely/alarms/TimeChangeDetector.java b/app/src/main/java/com/noah/timely/alarms/TimeChangeDetector.java
index 42818fa4..f23ee653 100644
--- a/app/src/main/java/com/noah/timely/alarms/TimeChangeDetector.java
+++ b/app/src/main/java/com/noah/timely/alarms/TimeChangeDetector.java
@@ -21,7 +21,7 @@
import java.util.Locale;
/**
- * The thread responsible for blinking the colon in between the minute and second indicator
+ * The thread responsible for detecting in global time and date changes
*/
public class TimeChangeDetector extends Thread {
diff --git a/app/src/main/java/com/noah/timely/assignment/AddAssignmentActivity.java b/app/src/main/java/com/noah/timely/assignment/AddAssignmentActivity.java
index b9e96f57..6fe85cf4 100644
--- a/app/src/main/java/com/noah/timely/assignment/AddAssignmentActivity.java
+++ b/app/src/main/java/com/noah/timely/assignment/AddAssignmentActivity.java
@@ -295,7 +295,7 @@ private void saveOrUpdateAssignment() {
data = new AssignmentModel(pos, ln, tt, description, date, cc, date, attachedPDF, attachedImage, false);
- if (database.isAssignmentPresent(data) && !tryScheduleNotifiers(yy, mm, dd, tt, ln, data, pos)) {
+ if (!database.isAssignmentAbsent(data) && !tryScheduleNotifiers(yy, mm, dd, tt, ln, data, pos)) {
ErrorDialog.Builder errorBuilder = new ErrorDialog.Builder();
errorBuilder.setDialogMessage("Invalid assignment")
.setShowSuggestions(true)
diff --git a/app/src/main/java/com/noah/timely/assignment/AssignmentFragment.java b/app/src/main/java/com/noah/timely/assignment/AssignmentFragment.java
index 074fe630..fbb78cf6 100644
--- a/app/src/main/java/com/noah/timely/assignment/AssignmentFragment.java
+++ b/app/src/main/java/com/noah/timely/assignment/AssignmentFragment.java
@@ -37,7 +37,9 @@
import com.noah.timely.core.RequestParams;
import com.noah.timely.core.RequestRunner;
import com.noah.timely.core.SchoolDatabase;
+import com.noah.timely.exports.TMLYDataGeneratorDialog;
import com.noah.timely.util.CollectionUtils;
+import com.noah.timely.util.Constants;
import com.noah.timely.util.ThreadUtils;
import org.greenrobot.eventbus.EventBus;
@@ -193,7 +195,7 @@ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflat
itemCount = layout.findViewById(R.id.counter);
itemCount.setText(String.valueOf(aList.size()));
menu.findItem(R.id.select_all).setVisible(aList.isEmpty() ? false : true);
- TooltipCompat.setTooltipText(itemCount, "Assignment Count");
+ TooltipCompat.setTooltipText(itemCount, getString(R.string.assignment_count) + aList.size());
super.onCreateOptionsMenu(menu, inflater);
}
@@ -207,6 +209,8 @@ public void onPrepareOptionsMenu(@NonNull Menu menu) {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.select_all) {
assignmentAdapter.selectAllItems();
+ } else if (item.getItemId() == R.id.export) {
+ new TMLYDataGeneratorDialog().show(getContext(), Constants.ASSIGNMENT);
}
return super.onOptionsItemSelected(item);
}
diff --git a/app/src/main/java/com/noah/timely/assignment/AssignmentModel.java b/app/src/main/java/com/noah/timely/assignment/AssignmentModel.java
index 74c548b9..1c6bf845 100644
--- a/app/src/main/java/com/noah/timely/assignment/AssignmentModel.java
+++ b/app/src/main/java/com/noah/timely/assignment/AssignmentModel.java
@@ -46,23 +46,6 @@ public void setChronologicalOrder(int chronologicalOrder) {
this.chronologicalOrder = chronologicalOrder;
}
- @Override
- @SuppressWarnings("all")
- public String toString() {
- return "AssignmentModel{" +
- "id=" + super.position +
- ", courseCode='" + courseCode + '\'' +
- ", lecturerName='" + lecturerName + '\'' +
- ", title='" + title + '\'' +
- ", description='" + description + '\'' +
- ", date='" + date + '\'' +
- ", submissionDate='" + submissionDate + '\'' +
- ", attachedPDF='" + attachedPDF + '\'' +
- ", attachedImage='" + attachedImage + '\'' +
- ", isSubmitted=" + isSubmitted +
- '}';
- }
-
public boolean isSubmitted() {
return isSubmitted;
}
@@ -127,7 +110,7 @@ public void setDescription(String description) {
this.description = description;
}
- String getDate() {
+ public String getDate() {
return date;
}
@@ -142,4 +125,22 @@ public int getChangeId() {
public void setId(int id) {
this.id = id;
}
+
+ @Override
+ @SuppressWarnings("all")
+ public String toString() {
+ return "AssignmentModel{" +
+ "chronologicalOrder=" + chronologicalOrder +
+ ", courseCode='" + courseCode + '\'' +
+ ", lecturerName='" + lecturerName + '\'' +
+ ", title='" + title + '\'' +
+ ", description='" + description + '\'' +
+ ", date='" + date + '\'' +
+ ", submissionDate='" + submissionDate + '\'' +
+ ", attachedPDF='" + attachedPDF + '\'' +
+ ", attachedImage='" + attachedImage + '\'' +
+ ", isSubmitted=" + isSubmitted +
+ ", id=" + id +
+ '}';
+ }
}
diff --git a/app/src/main/java/com/noah/timely/assignment/AssignmentRowHolder.java b/app/src/main/java/com/noah/timely/assignment/AssignmentRowHolder.java
index e1b8fb22..a057105d 100644
--- a/app/src/main/java/com/noah/timely/assignment/AssignmentRowHolder.java
+++ b/app/src/main/java/com/noah/timely/assignment/AssignmentRowHolder.java
@@ -60,7 +60,7 @@ public class AssignmentRowHolder extends RecyclerView.ViewHolder {
private List aList;
private boolean isChecked;
- @SuppressWarnings({"ClickableViewAccessibility", "ConstantConditions"})
+ @SuppressWarnings({ "ClickableViewAccessibility", "ConstantConditions" })
public AssignmentRowHolder(@NonNull View rootView) {
super(rootView);
header = rootView.findViewById(R.id.header);
@@ -75,11 +75,7 @@ public AssignmentRowHolder(@NonNull View rootView) {
viewButton = rootView.findViewById(R.id.viewButton);
img_stats = rootView.findViewById(R.id.stats);
- viewButton.setOnClickListener(v -> {
- AssignmentModel assignment = (AssignmentModel) aList.get(getAbsoluteAdapterPosition());
- assignment.setChronologicalOrder(getAbsoluteAdapterPosition());
- new AssignmentViewDialog().show(mActivity, assignment);
- });
+ viewButton.setOnClickListener(v -> viewFullAssignmentDetails());
deleteButton.setOnClickListener(v -> doDeleteAssignment());
@@ -119,6 +115,8 @@ public AssignmentRowHolder(@NonNull View rootView) {
if (assignmentRowAdapter.getCheckedAssignmentsCount() == 0) {
assignmentRowAdapter.setMultiSelectionEnabled(false);
}
+ } else {
+ viewFullAssignmentDetails();
}
});
}
@@ -169,12 +167,19 @@ public void bindView() {
img_stats.setImageResource(assignment.isSubmitted() ? R.drawable.ic_round_check_circle
: R.drawable.ic_pending);
- TooltipCompat.setTooltipText(img_stats, "Submission status");
+ TooltipCompat.setTooltipText(img_stats, "Submission status: "
+ + (assignment.isSubmitted() ? "SUBMITTED" : "PENDING"));
isChecked = assignmentRowAdapter.isChecked(getAbsoluteAdapterPosition());
v_selectionOverlay.setVisibility(isChecked ? View.VISIBLE : View.GONE);
tryDisableViews(assignmentRowAdapter.isMultiSelectionEnabled());
}
+ private void viewFullAssignmentDetails() {
+ AssignmentModel assignment = (AssignmentModel) aList.get(getAbsoluteAdapterPosition());
+ assignment.setChronologicalOrder(getAbsoluteAdapterPosition());
+ new AssignmentViewDialog().show(mActivity, assignment);
+ }
+
// Determines if there was an added title in the lecturer's name
private boolean startsWithAny(String[] titles, String s) {
for (String title : titles) if (s.startsWith(title)) return true;
@@ -187,8 +192,8 @@ private boolean startsWithAny(String[] titles, String s) {
private String truncateName(String fullName) {
String[] nameTokens = fullName.split(" ");
- String[] titles = {"Barr", "Barrister", "Doc", "Doctor", "Dr", "Engineer", "Engr", "Mr",
- "Mister", "Mrs", "Ms", "Prof", "Professor"};
+ String[] titles = { "Barr", "Barrister", "Doc", "Doctor", "Dr", "Engineer", "Engr", "Mr",
+ "Mister", "Mrs", "Ms", "Prof", "Professor" };
StringBuilder nameBuilder = new StringBuilder();
diff --git a/app/src/main/java/com/noah/timely/core/DataModel.java b/app/src/main/java/com/noah/timely/core/DataModel.java
index 7d861155..8d688fce 100644
--- a/app/src/main/java/com/noah/timely/core/DataModel.java
+++ b/app/src/main/java/com/noah/timely/core/DataModel.java
@@ -34,4 +34,14 @@ public int getUID() {
public void setUID(int uid) {
this.uid = uid;
}
+
+ @Override
+ @SuppressWarnings("all")
+ public String toString() {
+ return "DataModel{" +
+ "id=" + id +
+ ", position=" + position +
+ ", uid=" + uid +
+ '}';
+ }
}
diff --git a/app/src/main/java/com/noah/timely/core/MultiChoiceMode.java b/app/src/main/java/com/noah/timely/core/MultiChoiceMode.java
index afeecfc4..ba9b794f 100644
--- a/app/src/main/java/com/noah/timely/core/MultiChoiceMode.java
+++ b/app/src/main/java/com/noah/timely/core/MultiChoiceMode.java
@@ -14,7 +14,7 @@ public class MultiChoiceMode implements ChoiceMode {
public static final String ARG_POS_INDICES = "Position indices";
public static final String ARG_ID_INDICES = "Id Indices";
- // States to be saved
+ // States to be saved or parceled
protected List indices = new ArrayList<>();
protected List indices2 = new ArrayList<>();
protected ParcelableSparseBooleanArray sbarr = new ParcelableSparseBooleanArray();
@@ -59,4 +59,15 @@ public Integer[] getCheckedChoicesIndices() {
public Integer[] getCheckedChoicePositions() {
return indices2.toArray(new Integer[0]);
}
+
+ public void setChecked(int position, boolean isChecked) {
+ if (!isChecked) {
+ sbarr.delete(position);
+ indices.remove(Integer.valueOf(position));
+ } else {
+ sbarr.put(position, true);
+ indices.add(position);
+ }
+ }
+
}
\ No newline at end of file
diff --git a/app/src/main/java/com/noah/timely/core/SchoolDatabase.java b/app/src/main/java/com/noah/timely/core/SchoolDatabase.java
index 3e8871d1..8ee63a5f 100644
--- a/app/src/main/java/com/noah/timely/core/SchoolDatabase.java
+++ b/app/src/main/java/com/noah/timely/core/SchoolDatabase.java
@@ -26,6 +26,7 @@
import com.noah.timely.util.CollectionUtils;
import com.noah.timely.util.Constants;
import com.noah.timely.util.LogUtils;
+import com.noah.timely.util.PreferenceUtils;
import com.noah.timely.util.ThreadUtils;
import java.util.ArrayList;
@@ -96,6 +97,7 @@ public class SchoolDatabase extends SQLiteOpenHelper {
private static final String COLUMN_TODO_TITLE = "Title";
private static final String COLUMN_TODO_DATE = "Completion_date";
private static final String COLUMN_TODO_TIME = "Completion_time";
+ private static final String COLUMN_SEMESTER = "Semester";
private static boolean mDeleting;
@@ -104,10 +106,19 @@ public class SchoolDatabase extends SQLiteOpenHelper {
private final Context context;
public SchoolDatabase(@Nullable Context context) {
- super(context, "SchoolDatabase.db", null, 2);
+ // remember to update the database version specified in the super class' constructor
+ super(context, "SchoolDatabase.db", null, 3);
this.context = context;
}
+ /**
+ * @return the database version
+ */
+ public int getDatabaseVersion() {
+ SQLiteDatabase db = getReadableDatabase();
+ return db.getVersion();
+ }
+
/**
* Temporary fix to bug that exists when app crashes as a result of improper ended background task
*
@@ -147,13 +158,6 @@ public void onCreate(SQLiteDatabase db) {
createTodoListTables(db);
createAssignmentTable(db);
createAlarmTable(db);
- createPrefTable(db);
- // Insert default value in exam week count. Although it is not useful, as value will never
- // be retrieved, this is useful to use the update database operation on this table.
- ContentValues values = new ContentValues();
- values.put(COLUMN_EXAM_WEEK_COUNT, 8);
-
- db.insertOrThrow(PREFERENCE_TABLE, null, values);
}
@Override
@@ -163,16 +167,13 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// bunmp version from v1.0 to v2.0. In v1.0, _Todo Table does not exist, so upgrade former version's
// database, adding the _Todo table to begin data insertion.
createTodoListTables(db);
+ } else if ((oldVersion == 1 || oldVersion == 2) && newVersion == 3) {
+ // drop Preference_Table in version 3.0 and replace with androidx preference
+ db.execSQL("DROP TABLE IF EXISTS " + PREFERENCE_TABLE);
}
}
- // CREATE PREFS TABLE
- private void createPrefTable(SQLiteDatabase db) {
- String createPrefsDB_stmt = "CREATE TABLE " + PREFERENCE_TABLE + " (" + COLUMN_EXAM_WEEK_COUNT + " INTEGER )";
- db.execSQL(createPrefsDB_stmt);
- }
-
// CREATE ALARM TABLE
private void createAlarmTable(SQLiteDatabase db) {
String createAlarmDB_stmt = "CREATE TABLE " + ALARMS_TABLE + " (" +
@@ -192,7 +193,6 @@ private void createAlarmTable(SQLiteDatabase db) {
db.execSQL(createAlarmDB_stmt);
}
-
// CREATE ASSIGNMENT DATA TABLE
private void createAssignmentTable(SQLiteDatabase db) {
String createAssignmentDB_stmt
@@ -265,6 +265,7 @@ private void createExamTables(SQLiteDatabase db) {
private void createSemesterTable(SQLiteDatabase db, String semester) {
String createSemesterTable_stmt = "CREATE TABLE " + semester + " (" +
COLUMN_ID + " INTEGER, " +
+ COLUMN_SEMESTER + " TEXT, " +
COLUMN_COURSE_CODE + " TEXT, " +
COLUMN_FULL_COURSE_NAME + " TEXT, " +
COLUMN_CREDITS + " INTEGER"
@@ -335,7 +336,7 @@ public boolean addAssignmentData(AssignmentModel assignment) {
*
* @return true if a duplicate assignment was found
*/
- public boolean isAssignmentPresent(AssignmentModel model) {
+ public boolean isAssignmentAbsent(AssignmentModel model) {
String findAssignmentStmt = "SELECT * FROM " + ASSIGNMENT_TABLE
+ " WHERE " + COLUMN_ASSIGNMENT_TITLE + " = '" + sanitizeEntry(model.getTitle())
+ "' AND " + COLUMN_SUBMISSION_DATE + " = '" + model.getSubmissionDate()
@@ -345,9 +346,9 @@ public boolean isAssignmentPresent(AssignmentModel model) {
SQLiteDatabase db = getReadableDatabase();
Cursor findCursor = db.rawQuery(findAssignmentStmt, null);
+ boolean isAbsent = findCursor.getCount() == 0;
findCursor.close();
-
- return findCursor.getColumnCount() > 0;
+ return isAbsent;
}
private String sanitizeEntry(String data) {
@@ -672,7 +673,6 @@ public List getTimeTableData(String timetableName) {
return data;
}
-
/**
* Description
* This method deletes assignment from the app's database
@@ -1412,34 +1412,54 @@ public int getCoursesCount(String SEMESTER) {
/**
* Retrieves the number of registered courses for a particular semester
*
- * @param SEMESTER the semester in which it's data is to be retrieved
+ * @param SEMESTER the semester in which it's data is to be retrieved, specify null
to combine both
+ * FIRST and SECOND semester.
* @return all registered courses in that semester in alphabetic order of course names
*/
public List getCoursesData(String SEMESTER) {
List courseData = new ArrayList<>();
SQLiteDatabase db = getReadableDatabase();
- String coursesQuery_stmt =
- "SELECT " + COLUMN_ID
- + ", " + COLUMN_CREDITS
- + ", " + COLUMN_COURSE_CODE
- + ", " + COLUMN_FULL_COURSE_NAME
- + " FROM " + SEMESTER
- + " ORDER BY " + COLUMN_FULL_COURSE_NAME;
+ String coursesQuery_stmt = null;
+
+ if (SEMESTER != null) {
+ coursesQuery_stmt =
+ "SELECT " + COLUMN_ID
+ + ", " + COLUMN_SEMESTER
+ + ", " + COLUMN_CREDITS
+ + ", " + COLUMN_COURSE_CODE
+ + ", " + COLUMN_FULL_COURSE_NAME
+ + " FROM " + SEMESTER
+ + " ORDER BY " + COLUMN_FULL_COURSE_NAME;
+
+ } else {
+ coursesQuery_stmt = " SELECT " + COLUMN_ID
+ + ", " + COLUMN_SEMESTER
+ + ", " + COLUMN_CREDITS
+ + ", " + COLUMN_COURSE_CODE
+ + ", " + COLUMN_FULL_COURSE_NAME
+ + " FROM " + FIRST_SEMESTER
+ + " UNION ALL " +
+ " SELECT " + COLUMN_ID
+ + ", " + COLUMN_SEMESTER
+ + ", " + COLUMN_CREDITS
+ + ", " + COLUMN_COURSE_CODE
+ + ", " + COLUMN_FULL_COURSE_NAME
+ + " FROM " + SECOND_SEMESTER
+ + " ORDER BY " + COLUMN_FULL_COURSE_NAME;
+ }
if (!db.isOpen()) db = getReadableDatabase();
Cursor courseCursor = db.rawQuery(coursesQuery_stmt, null);
while (courseCursor.moveToNext()) {
CourseModel courseModel = new CourseModel();
- courseModel.setSemester(SEMESTER);
- courseModel.setCourseCode(retrieveEntry(courseCursor.getString(2)));
- courseModel.setCourseName(retrieveEntry(courseCursor.getString(3)));
- courseModel.setCredits(courseCursor.getInt(1));
+ courseModel.setSemester(retrieveEntry(courseCursor.getString(1)));
+ courseModel.setCourseCode(retrieveEntry(courseCursor.getString(3)));
+ courseModel.setCourseName(retrieveEntry(courseCursor.getString(4)));
+ courseModel.setCredits(courseCursor.getInt(2));
courseModel.setId(courseCursor.getInt(0));
courseData.add(courseModel);
}
- // reverse the order of alarms to show the most recent alarm
-// Collections.reverse(alarms);
courseCursor.close();
return courseData;
}
@@ -1464,6 +1484,7 @@ public int[] addCourse(CourseModel courseModel, String SEMESTER) {
ContentValues courseValues = new ContentValues();
int insertPos = ++lastID;
courseValues.put(COLUMN_ID, insertPos);
+ courseValues.put(COLUMN_SEMESTER, courseModel.getSemester());
courseValues.put(COLUMN_CREDITS, courseModel.getCredits());
courseValues.put(COLUMN_COURSE_CODE, sanitizeEntry(courseModel.getCourseCode()));
courseValues.put(COLUMN_FULL_COURSE_NAME, sanitizeEntry(courseModel.getCourseName()));
@@ -1633,50 +1654,93 @@ public boolean isExamAbsent(String WEEK, ExamModel model) {
* and don't exist yet, so create them, when user wants to use them.
*
* @param index the timetable to be retrieved with index 0, meaning
- * WEEK 1
+ * WEEK 1. NOTE: specifying an argument of -1 will join all the
+ * timetables together
* @return empty data if table doesn't exists yet and then create it or returns the current
* exam timetable data in database
*/
- public List getExamTimetableDataFor(int index) {
- String examTable = "WEEK_" + (index + 1);
+ public List getExamTimetableDataForWeek(int index) {
SQLiteDatabase db = getReadableDatabase();
- // Create this table if it doesn't exist
- String createExamTables_stmt = "CREATE TABLE IF NOT EXISTS " + examTable + " (" +
- COLUMN_ID + " INTEGER, " +
- COLUMN_WEEK + " TEXT, " +
- COLUMN_DAY + " TEXT, " +
- COLUMN_COURSE_CODE + " TEXT, " +
- COLUMN_FULL_COURSE_NAME + " TEXT, " +
- COLUMN_START_TIME + " INTEGER, " +
- COLUMN_END_TIME + " INTEGER"
- + ")";
+ List exams = new ArrayList<>();
+ // index = -1 fetches all the exam timetables present
+ if (index != -1) {
+ String examTable = "WEEK_" + (index + 1);
+ // Create this table if it doesn't exist
+ String createExamTables_stmt = "CREATE TABLE IF NOT EXISTS " + examTable + " (" +
+ COLUMN_ID + " INTEGER, " +
+ COLUMN_WEEK + " TEXT, " +
+ COLUMN_DAY + " TEXT, " +
+ COLUMN_COURSE_CODE + " TEXT, " +
+ COLUMN_FULL_COURSE_NAME + " TEXT, " +
+ COLUMN_START_TIME + " INTEGER, " +
+ COLUMN_END_TIME + " INTEGER"
+ + ")";
+
+ db.execSQL(createExamTables_stmt);
- db.execSQL(createExamTables_stmt);
+ String getExams_stmt
+ = "SELECT " + COLUMN_ID + ","
+ + COLUMN_COURSE_CODE + ","
+ + COLUMN_FULL_COURSE_NAME + ","
+ + COLUMN_START_TIME + ","
+ + COLUMN_END_TIME + ","
+ + COLUMN_WEEK + ","
+ + COLUMN_DAY
+ + " FROM " + examTable;
+
+ Cursor getExamsCursor = db.rawQuery(getExams_stmt, null);
+ while (getExamsCursor.moveToNext()) {
+ int id = getExamsCursor.getInt(0);
+ String courseCode = getExamsCursor.getString(1);
+ String courseName = retrieveEntry(getExamsCursor.getString(2));
+ String startTime = getExamsCursor.getString(3);
+ String endTime = getExamsCursor.getString(4);
+ String examWeek = getExamsCursor.getString(5);
+ String day = getExamsCursor.getString(6);
+ exams.add(new ExamModel(day, examWeek, id, courseCode, courseName, startTime, endTime));
+ }
+ getExamsCursor.close();
- List exams = new ArrayList<>();
+ } else {
+
+ // query database, merge all exam timetables together all into one
+ // get the amount of exam timetable weeks present
+ int weekCount = PreferenceUtils.getIntegerValue(getContext(), COLUMN_EXAM_WEEK_COUNT, 8);
+ StringBuilder selectStmtBuilder = new StringBuilder();
+
+ for (int i = 1; i <= weekCount; i++) {
+
+ String stmt = "SELECT " + COLUMN_ID + ","
+ + COLUMN_COURSE_CODE + ","
+ + COLUMN_FULL_COURSE_NAME + ","
+ + COLUMN_START_TIME + ","
+ + COLUMN_END_TIME + ","
+ + COLUMN_WEEK + ","
+ + COLUMN_DAY
+ + " FROM WEEK_";
+
+ selectStmtBuilder.append(stmt).append(i);
+ if (weekCount > 1 && i != weekCount) {
+ selectStmtBuilder.append(" UNION ALL ");
+ }
+ }
- String getExams_stmt
- = "SELECT " + COLUMN_ID + ","
- + COLUMN_COURSE_CODE + ","
- + COLUMN_FULL_COURSE_NAME + ","
- + COLUMN_START_TIME + ","
- + COLUMN_END_TIME + ","
- + COLUMN_WEEK + ","
- + COLUMN_DAY
- + " FROM " + examTable;
-
- Cursor getExamsCursor = db.rawQuery(getExams_stmt, null);
- while (getExamsCursor.moveToNext()) {
- int id = getExamsCursor.getInt(0);
- String courseCode = getExamsCursor.getString(1);
- String courseName = retrieveEntry(getExamsCursor.getString(2));
- String startTime = getExamsCursor.getString(3);
- String endTime = getExamsCursor.getString(4);
- String examWeek = getExamsCursor.getString(5);
- String day = getExamsCursor.getString(6);
- exams.add(new ExamModel(day, examWeek, id, courseCode, courseName, startTime, endTime));
+ // after building up the query according to the user selected week-count, retrieve the built-up
+ // string from the String Builder
+ Cursor getExamsCursor = db.rawQuery(selectStmtBuilder.toString(), null);
+ while (getExamsCursor.moveToNext()) {
+ int id = getExamsCursor.getInt(0);
+ String courseCode = getExamsCursor.getString(1);
+ String courseName = retrieveEntry(getExamsCursor.getString(2));
+ String startTime = getExamsCursor.getString(3);
+ String endTime = getExamsCursor.getString(4);
+ String examWeek = getExamsCursor.getString(5);
+ String day = getExamsCursor.getString(6);
+ exams.add(new ExamModel(day, examWeek, id, courseCode, courseName, startTime, endTime));
+ }
+ getExamsCursor.close();
}
- getExamsCursor.close();
+
return exams;
}
@@ -1697,23 +1761,17 @@ public void dropRedundantExamTables() {
} catch (NumberFormatException exc) {
Log.w(TAG, "Ignoring user selected Week count of: " + countValue + ", using 8 weeks instead ");
}
- Cursor getEndCursor = db.rawQuery("SELECT " + COLUMN_EXAM_WEEK_COUNT + " FROM " + PREFERENCE_TABLE, null);
// now perform the main operation of this action
int wEnd = wStart;
- if (getEndCursor.moveToFirst()) {
- wEnd = getEndCursor.getInt(0);
- }
+ wEnd = PreferenceUtils.getIntegerValue(getContext(), COLUMN_EXAM_WEEK_COUNT, 8);
if (wStart != wEnd) {
// drop all the unwanted tables, so as to reduce app's data
for (int i = wStart + 1; i <= wEnd; i++) {
db.execSQL("DROP TABLE " + "WEEK_" + i);
}
// Update week count to be retrieved later when dropping redundant tables
- ContentValues values = new ContentValues();
- values.put(COLUMN_EXAM_WEEK_COUNT, wStart);
- db.update(PREFERENCE_TABLE, values, null, null);
+ PreferenceUtils.setIntegerValue(getContext(), COLUMN_EXAM_WEEK_COUNT, wStart);
}
- getEndCursor.close();
db.close();
});
}
diff --git a/app/src/main/java/com/noah/timely/courses/CourseModel.java b/app/src/main/java/com/noah/timely/courses/CourseModel.java
index 80b23e98..6763aa54 100644
--- a/app/src/main/java/com/noah/timely/courses/CourseModel.java
+++ b/app/src/main/java/com/noah/timely/courses/CourseModel.java
@@ -1,6 +1,7 @@
package com.noah.timely.courses;
import com.noah.timely.core.DataModel;
+import com.noah.timely.core.SchoolDatabase;
@SuppressWarnings("unused")
public class CourseModel extends DataModel {
@@ -59,4 +60,20 @@ public String getSemester() {
public void setSemester(String semester) {
this.semester = semester;
}
+
+ @Override
+ @SuppressWarnings("all")
+ public String toString() {
+ return "CourseModel{" +
+ "semester='" + semester + '\'' +
+ ", credits=" + credits +
+ ", courseCode='" + courseCode + '\'' +
+ ", courseName='" + courseName + '\'' +
+ ", chronologicalOrder=" + chronologicalOrder +
+ '}';
+ }
+
+ public int getSemesterIndex() {
+ return semester.equals(SchoolDatabase.FIRST_SEMESTER) ? 0 : 1;
+ }
}
diff --git a/app/src/main/java/com/noah/timely/courses/SemesterFragment.java b/app/src/main/java/com/noah/timely/courses/SemesterFragment.java
index 13def062..fdce7c2a 100644
--- a/app/src/main/java/com/noah/timely/courses/SemesterFragment.java
+++ b/app/src/main/java/com/noah/timely/courses/SemesterFragment.java
@@ -37,7 +37,9 @@
import com.noah.timely.core.RequestParams;
import com.noah.timely.core.RequestRunner;
import com.noah.timely.core.SchoolDatabase;
+import com.noah.timely.exports.TMLYDataGeneratorDialog;
import com.noah.timely.util.CollectionUtils;
+import com.noah.timely.util.Constants;
import com.noah.timely.util.DeviceInfoUtil;
import com.noah.timely.util.ThreadUtils;
@@ -182,7 +184,7 @@ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflat
itemCount = layout.findViewById(R.id.counter);
itemCount.setText(String.valueOf(cList.size()));
menu.findItem(R.id.select_all).setVisible(cList.isEmpty() ? false : true);
- TooltipCompat.setTooltipText(itemCount, "Courses Count");
+ TooltipCompat.setTooltipText(itemCount, getString(R.string.courses_count) + cList.size());
super.onCreateOptionsMenu(menu, inflater);
}
@@ -196,6 +198,8 @@ public void onPrepareOptionsMenu(@NonNull Menu menu) {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.select_all) {
courseAdapter.selectAllItems();
+ } else if (item.getItemId() == R.id.export) {
+ new TMLYDataGeneratorDialog().show(getContext(), Constants.COURSE);
}
return super.onOptionsItemSelected(item);
}
diff --git a/app/src/main/java/com/noah/timely/custom/ExSpinner.java b/app/src/main/java/com/noah/timely/custom/ExSpinner.java
new file mode 100644
index 00000000..826137ce
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/custom/ExSpinner.java
@@ -0,0 +1,108 @@
+package com.noah.timely.custom;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatSpinner;
+import androidx.core.content.ContextCompat;
+
+import com.noah.timely.R;
+
+/**
+ * A custom spinner that tries to mimic Spotify Android app spinner functionality
+ */
+public class ExSpinner extends AppCompatSpinner {
+ private Paint linePaint, trianglePaint;
+ private final Point point1 = new Point();
+ private final Point point2 = new Point();
+ private final Point point3 = new Point();
+ private Path path;
+
+ public ExSpinner(@NonNull Context context) {
+ super(context);
+ init();
+ }
+
+ public ExSpinner(@NonNull Context context, int mode) {
+ super(context, mode);
+ init();
+ }
+
+ public ExSpinner(@NonNull Context context,
+ @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public ExSpinner(@NonNull Context context,
+ @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ public ExSpinner(@NonNull Context context,
+ @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs, int defStyleAttr, int mode) {
+ super(context, attrs, defStyleAttr, mode);
+ init();
+ }
+
+ public ExSpinner(@NonNull Context context,
+ @Nullable @org.jetbrains.annotations.Nullable AttributeSet attrs, int defStyleAttr, int mode,
+ Resources.Theme popupTheme) {
+ super(context, attrs, defStyleAttr, mode, popupTheme);
+ init();
+ }
+
+ private void init() {
+ linePaint = new Paint();
+ trianglePaint = new Paint();
+
+ linePaint.setStyle(Paint.Style.STROKE);
+ linePaint.setColor(ContextCompat.getColor(getContext(), android.R.color.darker_gray));
+ linePaint.setStrokeWidth(8.f);
+
+ trianglePaint.setColor(ContextCompat.getColor(getContext(), android.R.color.darker_gray));
+ trianglePaint.setStyle(Paint.Style.FILL);
+
+ path = new Path();
+ path.setFillType(Path.FillType.EVEN_ODD);
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ setBackground(null);
+ int width = getWidth();
+ int height = getHeight();
+ canvas.drawLine(0, height, width, height, linePaint);
+ path.moveTo(width, height);
+ path.lineTo(width, height - 20);
+ path.lineTo(width - 20, height);
+ path.close();
+ canvas.drawPath(path, trianglePaint);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ linePaint.setColor(ContextCompat.getColor(getContext(), R.color.accent));
+ trianglePaint.setColor(ContextCompat.getColor(getContext(), R.color.accent));
+ invalidate();
+ } else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
+ linePaint.setColor(ContextCompat.getColor(getContext(), android.R.color.darker_gray));
+ trianglePaint.setColor(ContextCompat.getColor(getContext(), android.R.color.darker_gray));
+ invalidate();
+ }
+
+ return super.onTouchEvent(event);
+ }
+
+}
diff --git a/app/src/main/java/com/noah/timely/error/ErrorDialog.java b/app/src/main/java/com/noah/timely/error/ErrorDialog.java
index 2cec6b4a..eaa89061 100644
--- a/app/src/main/java/com/noah/timely/error/ErrorDialog.java
+++ b/app/src/main/java/com/noah/timely/error/ErrorDialog.java
@@ -17,6 +17,9 @@
import com.noah.timely.R;
+/**
+ * Basic error dialog to display errors to the user
+ */
public class ErrorDialog extends DialogFragment implements View.OnClickListener {
private ErrorMessage err;
diff --git a/app/src/main/java/com/noah/timely/exam/ExamModel.java b/app/src/main/java/com/noah/timely/exam/ExamModel.java
index 9fc01b76..168ca519 100644
--- a/app/src/main/java/com/noah/timely/exam/ExamModel.java
+++ b/app/src/main/java/com/noah/timely/exam/ExamModel.java
@@ -45,6 +45,14 @@ public String getCourseCode() {
return courseCode;
}
+ public void setCourseCode(String courseCode) {
+ this.courseCode = courseCode;
+ }
+
+ public void setCourseName(String courseName) {
+ this.courseName = courseName;
+ }
+
public String getCourseName() {
return courseName;
}
@@ -110,8 +118,27 @@ public int getDayIndex() {
}
}
+ public int getWeekIndex() {
+ return Integer.parseInt(week.substring(week.indexOf('_') + 1)) - 1;
+ }
+
public int getStartAsInt() {
String[] ss = start.split(":");
return Integer.parseInt(ss[0] + ss[1]);
}
+
+ @Override
+ @SuppressWarnings("all")
+ public String toString() {
+ return "ExamModel{" +
+ "courseCode='" + courseCode + '\'' +
+ ", courseName='" + courseName + '\'' +
+ ", start='" + start + '\'' +
+ ", end='" + end + '\'' +
+ ", id=" + id +
+ ", chronologicalOrder=" + chronologicalOrder +
+ ", week='" + week + '\'' +
+ ", day='" + day + '\'' +
+ '}';
+ }
}
diff --git a/app/src/main/java/com/noah/timely/exam/ExamTimetableFragment.java b/app/src/main/java/com/noah/timely/exam/ExamTimetableFragment.java
index 4e13b0cd..b341387c 100644
--- a/app/src/main/java/com/noah/timely/exam/ExamTimetableFragment.java
+++ b/app/src/main/java/com/noah/timely/exam/ExamTimetableFragment.java
@@ -38,7 +38,9 @@
import com.noah.timely.core.RequestParams;
import com.noah.timely.core.RequestRunner;
import com.noah.timely.core.SchoolDatabase;
+import com.noah.timely.exports.TMLYDataGeneratorDialog;
import com.noah.timely.util.CollectionUtils;
+import com.noah.timely.util.Constants;
import com.noah.timely.util.DeviceInfoUtil;
import com.noah.timely.util.ThreadUtils;
@@ -98,7 +100,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
ThreadUtils.runBackgroundTask(() -> {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
- eList = database.getExamTimetableDataFor(position);
+ eList = database.getExamTimetableDataForWeek(position);
// Sort by start time
Collections.sort(eList, (e1, e2) -> {
ExamModel em1 = (ExamModel) e1;
@@ -194,7 +196,7 @@ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflat
itemCount = layout.findViewById(R.id.counter);
itemCount.setText(String.valueOf(eList.size()));
menu.findItem(R.id.select_all).setVisible(eList.isEmpty() ? false : true);
- TooltipCompat.setTooltipText(itemCount, "Exams Count");
+ TooltipCompat.setTooltipText(itemCount, getString(R.string.exams_count) + eList.size());
super.onCreateOptionsMenu(menu, inflater);
}
@@ -208,6 +210,8 @@ public void onPrepareOptionsMenu(@NonNull Menu menu) {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.select_all) {
examRowAdapter.selectAllItems();
+ } else if (item.getItemId() == R.id.export) {
+ new TMLYDataGeneratorDialog().show(getContext(), Constants.EXAM);
}
return super.onOptionsItemSelected(item);
}
diff --git a/app/src/main/java/com/noah/timely/exports/ActionProcessorDialog.java b/app/src/main/java/com/noah/timely/exports/ActionProcessorDialog.java
new file mode 100644
index 00000000..f68e6c22
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/exports/ActionProcessorDialog.java
@@ -0,0 +1,73 @@
+package com.noah.timely.exports;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.Window;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+
+import com.noah.timely.R;
+import com.noah.timely.util.ISupplier;
+import com.noah.timely.util.ThreadUtils;
+
+public class ActionProcessorDialog extends DialogFragment {
+ private static final String ARG_LIST = "list";
+ private static boolean dismiss_flag;
+ private ISupplier supplier;
+ private OnActionProcessedListener listener;
+
+ public ActionProcessorDialog execute(Context context, ISupplier supplier) {
+ this.supplier = supplier;
+ FragmentManager manager = ((FragmentActivity) context).getSupportFragmentManager();
+ show(manager, ActionProcessorDialog.class.getName());
+ return this;
+ }
+
+ public void setOnActionProcessedListener(OnActionProcessedListener listener) {
+ this.listener = listener;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ return new ProcessDialog(getContext());
+ }
+
+ private class ProcessDialog extends Dialog {
+
+ public ProcessDialog(@NonNull Context context) {
+ super(context, R.style.Dialog_No_Transition);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+ getWindow().setBackgroundDrawableResource(R.drawable.bg_rounded_edges);
+ setContentView(R.layout.dialog_processing);
+ ThreadUtils.runBackgroundTask(() -> {
+ supplier.get();
+ dismiss_flag = true;
+ getActivity().runOnUiThread(() -> {
+ if (listener != null) listener.onActionProcessed();
+ dismiss();
+ });
+ });
+ }
+
+ @Override
+ public void dismiss() {
+ if (dismiss_flag)
+ super.dismiss();
+ }
+ }
+
+ public interface OnActionProcessedListener {
+ void onActionProcessed();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/noah/timely/exports/ExportSuccessDialog.java b/app/src/main/java/com/noah/timely/exports/ExportSuccessDialog.java
new file mode 100644
index 00000000..11aa6435
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/exports/ExportSuccessDialog.java
@@ -0,0 +1,85 @@
+package com.noah.timely.exports;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.core.content.FileProvider;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+
+import com.noah.timely.BuildConfig;
+import com.noah.timely.R;
+
+import java.io.File;
+
+public class ExportSuccessDialog extends DialogFragment {
+ private static final String MESSAGE = "MESSAGE";
+ private static final String ARG_EXPORT_PATH = "EXPORT_PATH";
+
+ public void show(Context context, @StringRes int message, String exportPath) {
+ Bundle bundle = new Bundle();
+ bundle.putString(MESSAGE, context.getString(message));
+ bundle.putString(ARG_EXPORT_PATH, exportPath);
+ setArguments(bundle);
+ FragmentManager manager = ((FragmentActivity) context).getSupportFragmentManager();
+ show(manager, ExportSuccessDialog.class.getName());
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ return new DExportSuccessDialog(getContext());
+ }
+
+ private class DExportSuccessDialog extends Dialog {
+
+ public DExportSuccessDialog(@NonNull Context context) {
+ super(context, R.style.Dialog_Closeable);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+ getWindow().setBackgroundDrawableResource(R.drawable.bg_rounded_edges_8);
+ setContentView(R.layout.dialog_export_success);
+ TextView tv_message = findViewById(R.id.message);
+ Bundle arguments = getArguments();
+ tv_message.setText(arguments.getString(MESSAGE));
+
+ Button btn_share = findViewById(R.id.share);
+
+ String exportPath = getArguments().getString(ARG_EXPORT_PATH);
+ // can't send a file:// uri, transform it into a content:// uri
+ File file = new File(exportPath);
+ Uri fileUri = FileProvider.getUriForFile(getContext(), BuildConfig.APPLICATION_ID + ".provider", file);
+
+ // opens up chooser, used to send send the Uri of the exported file
+ btn_share.setOnClickListener(v -> {
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+
+ if (file.exists()) {
+ shareIntent.setType("application/tmly");
+ shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
+ shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ startActivity(Intent.createChooser(shareIntent, getString(R.string.share_text_subject_2)));
+ } else {
+ Toast.makeText(getActivity(), R.string.no_share_subject_text, Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/noah/timely/exports/FileImportDialog.java b/app/src/main/java/com/noah/timely/exports/FileImportDialog.java
new file mode 100644
index 00000000..e6b46106
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/exports/FileImportDialog.java
@@ -0,0 +1,87 @@
+package com.noah.timely.exports;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.Window;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+
+import com.noah.timely.R;
+import com.noah.timely.core.DataModel;
+import com.noah.timely.util.ThreadUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class FileImportDialog extends DialogFragment {
+ private static final String ARG_FILEPATH = "Import file path";
+ private static boolean dismiss_flag;
+ private OnResultReceivedListener listener;
+
+ public FileImportDialog execute(Context context, String filePath) {
+ Bundle bundle = new Bundle();
+ bundle.putString(ARG_FILEPATH, filePath);
+ setArguments(bundle);
+ FragmentManager manager = ((FragmentActivity) context).getSupportFragmentManager();
+ show(manager, FileImportDialog.class.getName());
+ return this;
+ }
+
+ public void setOnResultReceived(OnResultReceivedListener listener) {
+ this.listener = listener;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ return new ProcessDialog(getContext());
+ }
+
+ private class ProcessDialog extends Dialog {
+
+ public ProcessDialog(@NonNull Context context) {
+ super(context, R.style.Dialog_No_Transition);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+ getWindow().setBackgroundDrawableResource(R.drawable.bg_rounded_edges);
+ setContentView(R.layout.dialog_processing);
+ ThreadUtils.runBackgroundTask(this::doFileImport);
+ }
+
+ private void doFileImport() {
+ // generate data
+ Bundle arguments = getArguments();
+ Map> results
+ = TMLYFileGenerator.importFromFile(getContext(), getArguments().getString(ARG_FILEPATH));
+ // wierd, but I have to do it. I have to transform this map to a list of map entries.
+ List>> list = new ArrayList<>();
+ list.addAll(results.entrySet());
+ // run in ui thread - required
+ getActivity().runOnUiThread(() -> {
+ if (listener != null) listener.onResultReceived(list);
+ dismiss_flag = true;
+ dismiss(); // dismiss dialog if data was generated or not
+ });
+ }
+
+ @Override
+ public void dismiss() {
+ if (dismiss_flag)
+ super.dismiss();
+ }
+ }
+
+ public interface OnResultReceivedListener {
+ void onResultReceived(List>> results);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/noah/timely/exports/ImportListRowHolder.java b/app/src/main/java/com/noah/timely/exports/ImportListRowHolder.java
new file mode 100644
index 00000000..e0d4e7e2
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/exports/ImportListRowHolder.java
@@ -0,0 +1,106 @@
+package com.noah.timely.exports;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.noah.timely.R;
+import com.noah.timely.core.DataModel;
+import com.noah.timely.util.Constants;
+
+import java.util.List;
+import java.util.Map;
+
+@SuppressWarnings("FieldCanBeLocal")
+public class ImportListRowHolder extends RecyclerView.ViewHolder {
+ private int position;
+ private Context context;
+ private ImportResultsActivity.ImportListRowAdapter adapter;
+ private List>> datamap;
+ private final TextView tv_contentText, tv_contentCount;
+ private final View headerTop, headerBottom;
+ private final CheckBox cbx_state;
+
+ private static final int[] COLOR = {
+ android.R.color.holo_purple,
+ android.R.color.holo_green_light,
+ android.R.color.holo_blue_dark,
+ android.R.color.holo_orange_dark,
+ android.R.color.holo_red_light
+ };
+
+ private static final int[] DRAWABLE = {
+ R.drawable.bg_holo_purple_5,
+ R.drawable.bg_holo_green_dark_5,
+ R.drawable.bg_holo_blue_dark_5,
+ R.drawable.bg_holo_orange_dark_5,
+ R.drawable.bg_holo_red_light_5
+ };
+
+ public ImportListRowHolder(@NonNull View itemView) {
+ super(itemView);
+ tv_contentText = itemView.findViewById(R.id.content_text);
+ tv_contentCount = itemView.findViewById(R.id.content_count);
+ headerBottom = itemView.findViewById(R.id.header_bottom);
+ headerTop = itemView.findViewById(R.id.header_top);
+ cbx_state = itemView.findViewById(R.id.checkbox);
+
+ cbx_state.setOnCheckedChangeListener((v, isChecked) -> adapter.onChecked(position, isChecked));
+
+ }
+
+ public ImportListRowHolder with(int position, Context context,
+ ImportResultsActivity.ImportListRowAdapter adapter,
+ List>> datamap) {
+ this.position = position;
+ this.context = context;
+ this.adapter = adapter;
+ adapter.onChecked(position, true); // at the default, all the data would be imported if user doesn't toggle states
+ this.datamap = datamap;
+ return this;
+ }
+
+ public void bindView() {
+ Map.Entry> entry = datamap.get(position);
+ switch (entry.getKey() /* The data model constant */) {
+ case Constants.ASSIGNMENT:
+ tv_contentText.setText("Assignments");
+ headerTop.setBackgroundColor(ContextCompat.getColor(context, COLOR[0]));
+ headerBottom.setBackgroundColor(ContextCompat.getColor(context, COLOR[0]));
+ tv_contentCount.setBackground(ContextCompat.getDrawable(context, DRAWABLE[0]));
+ break;
+ case Constants.COURSE:
+ tv_contentText.setText("Registered Courses");
+ headerTop.setBackgroundColor(ContextCompat.getColor(context, COLOR[1]));
+ headerBottom.setBackgroundColor(ContextCompat.getColor(context, COLOR[1]));
+ tv_contentCount.setBackground(ContextCompat.getDrawable(context, DRAWABLE[1]));
+ break;
+ case Constants.EXAM:
+ tv_contentText.setText("Exams");
+ headerTop.setBackgroundColor(ContextCompat.getColor(context, COLOR[2]));
+ headerBottom.setBackgroundColor(ContextCompat.getColor(context, COLOR[2]));
+ tv_contentCount.setBackground(ContextCompat.getDrawable(context, DRAWABLE[2]));
+ break;
+ case Constants.TIMETABLE:
+ tv_contentText.setText("Timetable");
+ headerTop.setBackgroundColor(ContextCompat.getColor(context, COLOR[3]));
+ headerBottom.setBackgroundColor(ContextCompat.getColor(context, COLOR[3]));
+ tv_contentCount.setBackground(ContextCompat.getDrawable(context, DRAWABLE[3]));
+ break;
+ case Constants.SCHEDULED_TIMETABLE:
+ tv_contentText.setText("Scheduled Classes");
+ headerTop.setBackgroundColor(ContextCompat.getColor(context, COLOR[4]));
+ headerBottom.setBackgroundColor(ContextCompat.getColor(context, COLOR[4]));
+ tv_contentCount.setBackground(ContextCompat.getDrawable(context, DRAWABLE[4]));
+ break;
+ }
+
+ tv_contentCount.setText(String.valueOf(entry.getValue().size())); // list.size()
+ }
+
+}
diff --git a/app/src/main/java/com/noah/timely/exports/ImportResultsActivity.java b/app/src/main/java/com/noah/timely/exports/ImportResultsActivity.java
new file mode 100644
index 00000000..cf1223b9
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/exports/ImportResultsActivity.java
@@ -0,0 +1,494 @@
+package com.noah.timely.exports;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.Toast;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.noah.timely.R;
+import com.noah.timely.alarms.AlarmReSchedulerService;
+import com.noah.timely.assignment.AUpdateMessage;
+import com.noah.timely.assignment.AssignmentModel;
+import com.noah.timely.core.ChoiceMode;
+import com.noah.timely.core.DataModel;
+import com.noah.timely.core.MultiChoiceMode;
+import com.noah.timely.core.SchoolDatabase;
+import com.noah.timely.courses.CUpdateMessage;
+import com.noah.timely.courses.CourseModel;
+import com.noah.timely.error.ErrorDialog;
+import com.noah.timely.exam.EUpdateMessage;
+import com.noah.timely.exam.ExamModel;
+import com.noah.timely.io.IOUtils;
+import com.noah.timely.io.Zipper;
+import com.noah.timely.main.MainActivity;
+import com.noah.timely.scheduled.SUpdateMessage;
+import com.noah.timely.settings.SettingsActivity;
+import com.noah.timely.timetable.TUpdateMessage;
+import com.noah.timely.timetable.TimetableModel;
+import com.noah.timely.util.CollectionUtils;
+import com.noah.timely.util.Constants;
+import com.noah.timely.util.TimelyUpdateUtils;
+
+import org.greenrobot.eventbus.EventBus;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class ImportResultsActivity extends AppCompatActivity implements View.OnClickListener {
+ private ImportListRowAdapter listRowAdapter;
+ private boolean intentReceived;
+ private List>> entryList = new ArrayList<>();
+ private ViewGroup importView, initView, dataLayerView;
+ private final ActivityResultLauncher resourceChooserLauncher =
+ registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
+ if (result.getData() != null) {
+ Intent uploadfileIntent = result.getData();
+ File file = null;
+ try {
+ file = resolveDataToTempFile(uploadfileIntent.getData());
+ } catch (IOException e) {
+ Toast.makeText(this, "An internal error occurred", Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ if (file != null) {
+ performFileImport(file);
+ } else {
+ // import unsuccessful. Error occurred
+ ErrorDialog.Builder errorBuilder = new ErrorDialog.Builder();
+ errorBuilder.setShowSuggestions(true)
+ .setDialogMessage("An error occurred while importing data")
+ .setSuggestionCount(1)
+ .setSuggestion1("File extension might not be supported");
+
+ new ErrorDialog().showErrorMessage(this, errorBuilder.build());
+ }
+
+ }
+ });
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ int id = item.getItemId();
+ if (id == R.id.action_settings) {
+ startActivity(new Intent(this, SettingsActivity.class));
+ } else if (id == R.id.update) {
+ TimelyUpdateUtils.checkForUpdates(this);
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_import);
+
+ RecyclerView rv_importList = findViewById(R.id.import_list);
+ rv_importList.setHasFixedSize(true);
+ rv_importList.setLayoutManager(new LinearLayoutManager(this));
+ rv_importList.setAdapter((listRowAdapter = new ImportListRowAdapter(new MultiChoiceMode())));
+
+ Button btn_filePick = findViewById(R.id.file_pick), btn_importSelected = findViewById(R.id.import_selected);
+ importView = findViewById(R.id.import_view);
+ initView = findViewById(R.id.init_view);
+ dataLayerView = findViewById(R.id.data_layer);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setTitle(R.string.import_title);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ btn_importSelected.setOnClickListener(this);
+ btn_filePick.setOnClickListener(this);
+
+ Uri fileUri = null;
+ File file = null;
+
+ if ((fileUri = getIntent().getData()) != null) {
+ // make it possible to navigate to main activity on back pressed or on navigate up because the main activity
+ // isn't part of the activity stack
+ intentReceived = true;
+ try {
+ file = resolveUriToTempFile(fileUri);
+ } catch (IOException e) {
+ Toast.makeText(this, "An internal error occurred", Toast.LENGTH_LONG).show();
+ dataLayerView.setVisibility(View.GONE);
+ initView.setVisibility(View.GONE);
+ importView.setVisibility(View.VISIBLE);
+ }
+
+ // noinspection StatementWithEmptyBody
+ if (file != null) {
+ performFileImport(file);
+ } else {
+ IOUtils.deleteTempFiles(this);
+ dataLayerView.setVisibility(View.GONE);
+ initView.setVisibility(View.GONE);
+ importView.setVisibility(View.VISIBLE);
+ // import unsuccessful. Error occurred
+ ErrorDialog.Builder errorBuilder = new ErrorDialog.Builder();
+ errorBuilder.setShowSuggestions(true)
+ .setDialogMessage("An error occurred while importing data")
+ .setSuggestionCount(1)
+ .setSuggestion1("File extension might not be supported");
+
+ new ErrorDialog().showErrorMessage(this, errorBuilder.build());
+ }
+ } else {
+ dataLayerView.setVisibility(View.GONE);
+ initView.setVisibility(View.GONE);
+ importView.setVisibility(View.VISIBLE);
+ }
+
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ onBackPressed();
+ return super.onSupportNavigateUp();
+ }
+
+ @Override
+ public void onBackPressed() {
+ tryNavigateToMainActivity();
+ }
+
+ private void tryNavigateToMainActivity() {
+ if (intentReceived) {
+ intentReceived = false;
+ startActivity(new Intent(this, MainActivity.class));
+ // FIXME: 3/26/2022 remove the call to finish() on this activity and add the normal flag on the intent to
+ // finish the activity automatically
+ finish();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ listRowAdapter.getChoiceMode().onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ if (savedInstanceState != null)
+ listRowAdapter.getChoiceMode().onRestoreInstanceState(savedInstanceState);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view.getId() == R.id.file_pick) {
+ Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+ i.setType("*/*");
+ resourceChooserLauncher.launch(Intent.createChooser(i, getString(R.string.file_select_title)));
+ } else if (view.getId() == R.id.import_selected) {
+ // save to local database, removing duplicates
+ new ActionProcessorDialog()
+ .execute(this, this::persistSelectedToLocalDatabase)
+ .setOnActionProcessedListener(() -> {
+ // just a beautiful custom made Toast, but in dialog form :)
+ new ImportSuccessDialog().show(this, R.string.import_success_message);
+ });
+ }
+ }
+
+ private void persistSelectedToLocalDatabase() {
+ Integer[] indices = listRowAdapter.getChoiceMode().getCheckedChoicesIndices();
+ List>> filteredEntryList = new ArrayList<>();
+ for (int i = 0; i < indices.length; i++) {
+ filteredEntryList.add(entryList.get(indices[i]));
+ }
+
+ for (Map.Entry> entry : filteredEntryList) {
+ switch (entry.getKey() /* datamodel constant */) {
+ case Constants.ASSIGNMENT:
+ persistAssignments(entry.getValue());
+ break;
+ case Constants.COURSE:
+ persistCourses(entry.getValue());
+ break;
+ case Constants.EXAM:
+ persistExam(entry.getValue());
+ break;
+ case Constants.TIMETABLE:
+ persistTimetable(entry.getValue());
+ break;
+ case Constants.SCHEDULED_TIMETABLE:
+ persistScheduledTimetable(entry.getValue());
+ break;
+ default:
+ throw new IllegalStateException("Unexpected value: " + entry.getKey());
+ }
+ }
+
+ // after persisting to local database, use a start a service to schedule notifications for event remninders
+ Intent serviceIntent = new Intent(this, AlarmReSchedulerService.class);
+ serviceIntent.setAction(Constants.ACTION.SHOW_NOTIFICATION);
+ startService(serviceIntent);
+ }
+
+ private void persistScheduledTimetable(List extends DataModel> datamodelList) {
+ SchoolDatabase database = new SchoolDatabase(this);
+ EventBus eventBus = EventBus.getDefault();
+
+ for (DataModel data : datamodelList) {
+ TimetableModel timetable = (TimetableModel) data;
+ if (database.isTimeTableAbsent(SchoolDatabase.SCHEDULED_TIMETABLE, timetable)) {
+ int[] d = database.addTimeTableData(timetable, SchoolDatabase.SCHEDULED_TIMETABLE);
+ // post an application wide event to notify datamodel lists of change in order of list items
+ // after sorting it's elements in database
+ if (eventBus.hasSubscriberForEvent(SUpdateMessage.class)) {
+ timetable.setChronologicalOrder(d[0]);
+ timetable.setId(d[1]);
+ eventBus.post(new SUpdateMessage(timetable, SUpdateMessage.EventType.NEW));
+ }
+ }
+ }
+ }
+
+ private void persistTimetable(List extends DataModel> datamodelList) {
+ SchoolDatabase database = new SchoolDatabase(this);
+ EventBus eventBus = EventBus.getDefault();
+
+ for (DataModel data : datamodelList) {
+ TimetableModel timetable = (TimetableModel) data;
+ if (database.isTimeTableAbsent(timetable.getDay(), timetable)) {
+ int[] d = database.addTimeTableData(timetable, timetable.getDay());
+ // post an application wide event to notify datamodel lists of change in order of list items
+ // after sorting it's elements in database
+ if (eventBus.hasSubscriberForEvent(TUpdateMessage.class)) {
+ timetable.setChronologicalOrder(d[0]);
+ timetable.setId(d[1]);
+ int pagePosition = getPagePostion(timetable);
+ eventBus.post(new TUpdateMessage(timetable, pagePosition, TUpdateMessage.EventType.NEW));
+ }
+ }
+ }
+ }
+
+ private void persistExam(List extends DataModel> datamodelList) {
+ SchoolDatabase database = new SchoolDatabase(this);
+ EventBus eventBus = EventBus.getDefault();
+
+ for (DataModel data : datamodelList) {
+ ExamModel exam = (ExamModel) data;
+ if (database.isExamAbsent(exam.getWeek(), exam)) {
+ int[] d = database.addExam(exam, exam.getWeek());
+ // post an application wide event to notify datamodel lists of change in order of list items
+ // after sorting it's elements in database
+ if (eventBus.hasSubscriberForEvent(EUpdateMessage.class)) {
+ exam.setChronologicalOrder(d[0]);
+ exam.setId(d[1]);
+ int pagePosition = getPagePostion(exam);
+ eventBus.post(new EUpdateMessage(exam, EUpdateMessage.EventType.NEW, pagePosition));
+ }
+ }
+ }
+ }
+
+ private void persistCourses(List extends DataModel> datamodelList) {
+ SchoolDatabase database = new SchoolDatabase(this);
+ EventBus eventBus = EventBus.getDefault();
+
+ for (DataModel data : datamodelList) {
+ CourseModel course = (CourseModel) data;
+ if (database.isCourseAbsent(course)) {
+ int[] d = database.addCourse(course, course.getSemester());
+ // post an application wide event to notify datamodel lists of change in order of list items
+ // after sorting it's elements in database
+ if (eventBus.hasSubscriberForEvent(CUpdateMessage.class)) {
+ course.setChronologicalOrder(d[0]);
+ course.setId(d[1]);
+ int pagePosition = getPagePostion(course);
+ eventBus.post(new CUpdateMessage(course, CUpdateMessage.EventType.NEW, pagePosition));
+ }
+ }
+ }
+ }
+
+ private int getPagePostion(DataModel dataModel) {
+ if (dataModel instanceof ExamModel) {
+ ExamModel examModel = (ExamModel) dataModel;
+ return examModel.getWeekIndex();
+ } else if (dataModel instanceof TimetableModel) {
+ TimetableModel timetableModel = (TimetableModel) dataModel;
+ return timetableModel.getTimetableIndex();
+ } else if (dataModel instanceof CourseModel) {
+ CourseModel courseModel = (CourseModel) dataModel;
+ return courseModel.getSemesterIndex();
+ }
+
+ return 0;
+ }
+
+ private void persistAssignments(List extends DataModel> datamodelList) {
+ SchoolDatabase database = new SchoolDatabase(this);
+ EventBus eventBus = EventBus.getDefault();
+
+ for (DataModel data : datamodelList) {
+ AssignmentModel assignment = (AssignmentModel) data;
+ if (database.isAssignmentAbsent(assignment)) {
+ boolean b = database.addAssignmentData(assignment);
+ // post an application wide event to notify datamodel lists of change in order of list items
+ // after sorting it's elements in database
+ if (eventBus.hasSubscriberForEvent(AUpdateMessage.class)) {
+ eventBus.post(new AUpdateMessage(assignment, AUpdateMessage.EventType.NEW));
+ }
+ }
+ }
+ }
+
+ /*
+ * The resulting URI received from the Android device's file chooser would never be a correct URI to be used
+ * directly to get the file path because in Android, not all URIs points to a valid file. So a temp file
+ * was used to copy the data in the stream gotten from the URI, and then the temp file's path was used instead.
+ */
+ private File resolveDataToTempFile(Uri uri) throws IOException {
+ // return immediately if the file extension is not supported
+ if (!IOUtils.getFileExtension(new File(uri.getPath())).equals(Zipper.FILE_EXTENSION)) return null;
+
+ String parentFolder = getExternalFilesDir(null) + File.separator + "temp" + File.separator;
+ String tempFilePath = String.format(Locale.US, "%stemp%d.tmp", parentFolder, SystemClock.elapsedRealtime());
+
+ File tempFile = new File(tempFilePath);
+ File tempFileDir = tempFile.getParentFile();
+
+ boolean isCreated = true;
+ if (!tempFileDir.exists()) {
+ isCreated = tempFileDir.mkdirs();
+ }
+
+ if (!isCreated) return null;
+ else IOUtils.copy(getContentResolver().openInputStream(uri), new FileOutputStream(tempFile));
+
+ return tempFile;
+ }
+
+ /*
+ * The resulting URI received from the Android device's file chooser would never be a correct URI to be used
+ * directly to get the file path because in Android, not all URIs points to a valid file. So a temp file
+ * was used to copy the data in the stream gotten from the URI, and then the temp file's path was used instead.
+ */
+ private File resolveUriToTempFile(Uri uri) throws IOException {
+ String parentFolder = getExternalFilesDir(null) + File.separator + "temp" + File.separator;
+ String tempFilePath = String.format(Locale.US, "%stemp%d.tmp", parentFolder, SystemClock.elapsedRealtime());
+
+ File tempFile = new File(tempFilePath);
+ File tempFileDir = tempFile.getParentFile();
+
+ boolean isCreated = true;
+ if (!tempFileDir.exists()) {
+ isCreated = tempFileDir.mkdirs();
+ }
+
+ if (!isCreated) return null;
+ else IOUtils.copy(getContentResolver().openInputStream(uri), new FileOutputStream(tempFile));
+
+ return tempFile;
+ }
+
+ private void performFileImport(File file) {
+ dataLayerView.setVisibility(View.GONE);
+ initView.setVisibility(View.VISIBLE);
+ importView.setVisibility(View.GONE);
+ new FileImportDialog().execute(this, file.getAbsolutePath()).setOnResultReceived(results -> {
+ if (!CollectionUtils.isEmpty(results)) {
+ entryList = results;
+ // delete temp files used in file import operation, to free up memory in disk
+ boolean isDeleted = IOUtils.deleteTempFiles(this);
+ listRowAdapter.notifyDataSetChanged();
+ dataLayerView.setVisibility(View.VISIBLE);
+ initView.setVisibility(View.GONE);
+ importView.setVisibility(View.GONE);
+ // display count of data to be imported on titlebar
+ String message = "Found %d item" + (entryList.size() > 1 ? "s" : "");
+ getSupportActionBar().setTitle(String.format(Locale.US, message, entryList.size()));
+ } else {
+ dataLayerView.setVisibility(View.GONE);
+ initView.setVisibility(View.GONE);
+ importView.setVisibility(View.VISIBLE);
+ // import unsuccessful. error occurred
+ Toast.makeText(this, "IOException occurred, Try again", Toast.LENGTH_SHORT).show();
+
+ ErrorDialog.Builder errorBuilder = new ErrorDialog.Builder();
+ errorBuilder.setShowSuggestions(true)
+ .setDialogMessage("An empty data file was received")
+ .setSuggestionCount(2)
+ .setSuggestion1("Exported data might be empty")
+ .setSuggestion2("File might be corrupt");
+
+ new ErrorDialog().showErrorMessage(this, errorBuilder.build());
+ }
+ });
+ }
+
+ public class ImportListRowAdapter extends RecyclerView.Adapter {
+ private final ChoiceMode choiceMode;
+
+ public ImportListRowAdapter(ChoiceMode choiceMode) {
+ this.choiceMode = choiceMode;
+ }
+
+ public ChoiceMode getChoiceMode() {
+ return choiceMode;
+ }
+
+ public void onChecked(int position, boolean isChecked) {
+ MultiChoiceMode multiChoiceMode = (MultiChoiceMode) choiceMode;
+ multiChoiceMode.setChecked(position, isChecked);
+ }
+
+ public boolean isChecked(int position) {
+ return choiceMode.isChecked(position);
+ }
+
+ @NonNull
+ @Override
+ public ImportListRowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new ImportListRowHolder(getLayoutInflater().inflate(R.layout.import_list_row, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ImportListRowHolder holder, int position) {
+ holder.with(position, ImportResultsActivity.this, this, entryList).bindView();
+ }
+
+ @Override
+ public int getItemCount() {
+ return entryList.size();
+ }
+
+ }
+
+}
diff --git a/app/src/main/java/com/noah/timely/exports/ImportSuccessDialog.java b/app/src/main/java/com/noah/timely/exports/ImportSuccessDialog.java
new file mode 100644
index 00000000..123e0997
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/exports/ImportSuccessDialog.java
@@ -0,0 +1,58 @@
+package com.noah.timely.exports;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+
+import com.noah.timely.R;
+
+public class ImportSuccessDialog extends DialogFragment {
+ private static final String MESSAGE = "MESSAGE";
+
+ public void show(Context context, @StringRes int message) {
+ Bundle bundle = new Bundle();
+ bundle.putString(MESSAGE, context.getString(message));
+ setArguments(bundle);
+ FragmentManager manager = ((FragmentActivity) context).getSupportFragmentManager();
+ show(manager, ImportSuccessDialog.class.getName());
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ return new DExportSuccessDialog(getContext());
+ }
+
+ private class DExportSuccessDialog extends Dialog {
+
+ public DExportSuccessDialog(@NonNull Context context) {
+ super(context, R.style.Dialog_Closeable);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+ getWindow().setBackgroundDrawableResource(R.drawable.bg_rounded_edges_8);
+ setContentView(R.layout.dialog_import_success);
+ TextView tv_message = findViewById(R.id.message);
+ Bundle arguments = getArguments();
+ tv_message.setText(arguments.getString(MESSAGE));
+
+ Button btn_close = findViewById(R.id.close);
+ btn_close.setOnClickListener(v -> ImportSuccessDialog.this.dismiss());
+
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/noah/timely/exports/TMLYDataGeneratorDialog.java b/app/src/main/java/com/noah/timely/exports/TMLYDataGeneratorDialog.java
new file mode 100644
index 00000000..0b763095
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/exports/TMLYDataGeneratorDialog.java
@@ -0,0 +1,184 @@
+package com.noah.timely.exports;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+
+import com.noah.timely.R;
+import com.noah.timely.error.ErrorDialog;
+import com.noah.timely.util.Constants;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class TMLYDataGeneratorDialog extends DialogFragment {
+ private static final String ARG_DEFAULT_IDENTIFIER = "default_identifier";
+ private final List dataModelList = new ArrayList<>();
+ private Button btn_export;
+
+ public void show(Context context) {
+ FragmentManager manager = ((FragmentActivity) context).getSupportFragmentManager();
+ show(manager, TMLYDataGeneratorDialog.class.getName());
+ }
+
+ public void show(Context context, String defaultIdentifier) {
+ Bundle bundle = new Bundle();
+ bundle.putString(ARG_DEFAULT_IDENTIFIER, defaultIdentifier);
+ setArguments(bundle);
+ FragmentManager manager = ((FragmentActivity) context).getSupportFragmentManager();
+ show(manager, TMLYDataGeneratorDialog.class.getName());
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ return new DataGeneratorDialog(getContext());
+ }
+
+ private class DataGeneratorDialog extends Dialog implements CompoundButton.OnCheckedChangeListener {
+
+ public DataGeneratorDialog(@NonNull Context context) {
+ super(context, R.style.Dialog_Closeable);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+ getWindow().setBackgroundDrawableResource(R.drawable.bg_rounded_edges_8);
+ setContentView(R.layout.dialog_generate);
+
+ btn_export = findViewById(R.id.export);
+
+ btn_export.setOnClickListener(v -> {
+ new ActionProcessorDialog().execute(getActivity(), this::doExport);
+ hide();
+ });
+
+ ViewGroup vg_dataParent = findViewById(R.id.data_parent);
+ // Avoiding too much findViewById()'s, for quick load time
+ CheckBox cbx_courses, cbx_assignments, cbx_timetable, cbx_scheduled, cbx_exams;
+ cbx_courses = (CheckBox) vg_dataParent.getChildAt(0);
+ cbx_assignments = (CheckBox) vg_dataParent.getChildAt(1);
+ cbx_timetable = (CheckBox) vg_dataParent.getChildAt(2);
+ cbx_scheduled = (CheckBox) vg_dataParent.getChildAt(3);
+ cbx_exams = (CheckBox) vg_dataParent.getChildAt(4);
+ CheckBox[] checkBoxes = { cbx_courses, cbx_assignments, cbx_timetable, cbx_scheduled, cbx_exams };
+ // This is much more cleaner, than copy and pasting the same thing over again :)
+ for (int i = 0; i < checkBoxes.length; i++) checkBoxes[i].setOnCheckedChangeListener(this);
+
+ String defaultIdentifier = null;
+ if (getArguments() != null) {
+ defaultIdentifier = getArguments().getString(ARG_DEFAULT_IDENTIFIER);
+ }
+
+ if (!TextUtils.isEmpty(defaultIdentifier)) {
+ // de-select all checkboxes
+ for (int i = 0; i < checkBoxes.length; i++) checkBoxes[i].setChecked(false);
+ // select default checked checkbox
+ switch (defaultIdentifier) {
+ case Constants.COURSE:
+ cbx_courses.setChecked(true);
+ break;
+ case Constants.ASSIGNMENT:
+ cbx_assignments.setChecked(true);
+ break;
+ case Constants.TIMETABLE:
+ cbx_timetable.setChecked(true);
+ break;
+ case Constants.SCHEDULED_TIMETABLE:
+ cbx_scheduled.setChecked(true);
+ break;
+ case Constants.EXAM:
+ cbx_exams.setChecked(true);
+ break;
+ default:
+ throw new IllegalStateException("Unexpected value: " + defaultIdentifier);
+ }
+ } else {
+ // intially all checkboxes are checked
+ String[] datamodels = new String[]{ Constants.COURSE, Constants.ASSIGNMENT, Constants.TIMETABLE,
+ Constants.SCHEDULED_TIMETABLE, Constants.EXAM };
+ dataModelList.addAll(Arrays.asList(datamodels));
+ }
+ }
+
+ private void doExport() {
+ // generate data
+ Bundle arguments = getArguments();
+ String exportPath = TMLYFileGenerator.generate(getContext(), dataModelList);
+ // run in ui thread - required
+ getActivity().runOnUiThread(() -> {
+ if (exportPath != null) {
+ // Export successful, show result dialog
+ new ExportSuccessDialog().show(getActivity(), R.string.export_success_message, exportPath);
+
+ } else {
+ // Export unsuccessful. Error occurred
+ ErrorDialog.Builder errorBuilder = new ErrorDialog.Builder();
+ errorBuilder.setShowSuggestions(true)
+ .setDialogMessage("An Error occurred while generating data")
+ .setSuggestionCount(1)
+ .setSuggestion1("Check that you have enough memory");
+
+ new ErrorDialog().showErrorMessage(getActivity(), errorBuilder.build());
+ }
+
+ });
+
+ dismiss();
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ int id = buttonView.getId();
+
+ if (id == R.id.courses) {
+ // course check-bok
+ if (isChecked) dataModelList.add(Constants.COURSE);
+ else dataModelList.remove(Constants.COURSE);
+
+ } else if (id == R.id.assignments) {
+ // assignments check-box
+ if (isChecked) dataModelList.add(Constants.ASSIGNMENT);
+ else dataModelList.remove(Constants.ASSIGNMENT);
+
+ } else if (id == R.id.timetable) {
+ // timetable check-box
+ if (isChecked) dataModelList.add(Constants.TIMETABLE);
+ else dataModelList.remove(Constants.TIMETABLE);
+
+ } else if (id == R.id.scheduled_classes) {
+ // scheduled classes check-box
+ if (isChecked) dataModelList.add(Constants.SCHEDULED_TIMETABLE);
+ else dataModelList.remove(Constants.SCHEDULED_TIMETABLE);
+
+ } else if (id == R.id.exam_timetable) {
+ // exams check-box
+ if (isChecked) dataModelList.add(Constants.EXAM);
+ else dataModelList.remove(Constants.EXAM);
+ }
+
+ // disable export button, when user hasn't selected any data to be exported
+ if (dataModelList.isEmpty()) {
+ btn_export.setEnabled(false);
+ } else {
+ btn_export.setEnabled(true);
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/noah/timely/exports/TMLYFileGenerator.java b/app/src/main/java/com/noah/timely/exports/TMLYFileGenerator.java
new file mode 100644
index 00000000..ba04723c
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/exports/TMLYFileGenerator.java
@@ -0,0 +1,148 @@
+package com.noah.timely.exports;
+
+import static com.noah.timely.util.AppInfoUtils.getAppName;
+import static com.noah.timely.util.AppInfoUtils.getAppVesionName;
+import static com.noah.timely.util.AppInfoUtils.getDatabaseVerion;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.noah.timely.core.DataModel;
+import com.noah.timely.core.SchoolDatabase;
+import com.noah.timely.io.Zipper;
+import com.noah.timely.util.AppInfoUtils;
+import com.noah.timely.util.Constants;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * TimeLY's native .tmly file generator
+ */
+public class TMLYFileGenerator {
+
+ /**
+ * Generates an XML-based file of database
+ *
+ * @param context the context used in accessing resources
+ * @param dataModelIdentifierList the list of data to be tranformed
+ * @return the exported file path if file was saved, null otherwise
+ */
+ public static String generate(Context context, List dataModelIdentifierList) {
+ Map transformed = new HashMap<>();
+ transformed.put("Metadata", Transformer.getXML(createMetadataMap(context)));
+
+ for (int i = 0; i < dataModelIdentifierList.size(); i++) {
+ String hashKey = dataModelIdentifierList.get(i);
+ String xml_database = transformDatabaseToXML(context, hashKey);
+ // don't export an empty table
+ if (!TextUtils.isEmpty(xml_database))
+ transformed.put(hashKey, xml_database);
+ }
+
+ return writeTransformedDatabaseToFile(context, transformed);
+ }
+
+ /**
+ * Opens-up a valid .tmly file
+ *
+ * @param context the context used in accessing resources
+ * @param filePath the path to the file with the data to be imported
+ * @return an hash map containing the datamodel constants as keys and corresponding datamodel list as values
+ */
+ public static Map> importFromFile(Context context, String filePath) {
+ Map xmlStringMap = null;
+ try {
+ xmlStringMap = Zipper.unzipToXMLMap(context, filePath);
+ } catch (IOException e) {
+ return null;
+ }
+ return transformXMLMapToDatamodelMap(xmlStringMap);
+ }
+
+ private static Map> transformXMLMapToDatamodelMap(Map map) {
+ if (map == null) return null;
+ Map> datamap = new HashMap<>();
+ // key = data model constant equiv; value = xml as string to be transformed to a datamodel list
+ for (Map.Entry entry : map.entrySet()) {
+ // ignore the metadata in the file for now, it's not useful yet
+ if (entry.getKey().equals("Metadata.xml")) continue;
+ try {
+ datamap.put(entry.getKey(), Transformer.getDataModel(entry.getKey(), entry.getValue()));
+ } catch (Exception exc) {
+ Log.d(TMLYFileGenerator.class.getSimpleName(), "Exception: " + exc.getMessage());
+ return null; // file was corrupt, return null
+ }
+
+ }
+
+ return datamap;
+ }
+
+ private static String writeTransformedDatabaseToFile(Context context, Map transformed) {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("HHMMddmmyyyy");
+ Date date = new Date(System.currentTimeMillis()); // Set unique id for each file generated
+ String time = dateFormat.format(date);
+ String appName = AppInfoUtils.getAppName(context);
+ String folderName = "exported";
+
+ String output = String.format(Locale.US,
+ "%1$s%2$s%3$s%2$s%4$s-Data%5$s%6$s", context.getExternalFilesDir(null),
+ File.separator, folderName, appName, time, Zipper.FILE_EXTENSION);
+
+ boolean isCompressed = false;
+ try {
+ isCompressed = Zipper.zipXMLMap(context, transformed, output);
+ } catch (IOException e) {
+ return null;
+ }
+
+ return isCompressed ? output : null;
+ }
+
+ private static Map createMetadataMap(Context context) {
+ Map metadataMap = new HashMap<>();
+ metadataMap.put("app_name", getAppName(context));
+ metadataMap.put("app_version", getAppVesionName(context));
+ metadataMap.put("database_version", String.valueOf(getDatabaseVerion(context)));
+ return metadataMap;
+ }
+
+ private static String transformDatabaseToXML(Context context, String dataModelIdentifier) {
+ List dataModelList;
+ SchoolDatabase database = new SchoolDatabase(context);
+ Map dataModelListMap = new HashMap<>();
+
+ switch (dataModelIdentifier) {
+ case Constants.ASSIGNMENT:
+ dataModelList = database.getAssignmentData();
+ break;
+ case Constants.COURSE:
+ dataModelList = database.getCoursesData(null);
+ break;
+ case Constants.EXAM:
+ dataModelList = database.getExamTimetableDataForWeek(-1);
+ break;
+ case Constants.TIMETABLE:
+ dataModelList = database.getAllNormalSchoolTimetable();
+ break;
+ case Constants.SCHEDULED_TIMETABLE:
+ dataModelList = database.getTimeTableData(SchoolDatabase.SCHEDULED_TIMETABLE);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "The identifier " + dataModelIdentifier + " doesn't exists in database");
+ }
+
+ // key - dataModelIdentifier, value - dataModelList
+ return Transformer.getXML(new Object[]{ dataModelIdentifier, dataModelList });
+ }
+
+}
diff --git a/app/src/main/java/com/noah/timely/exports/Transformer.java b/app/src/main/java/com/noah/timely/exports/Transformer.java
new file mode 100644
index 00000000..bce95298
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/exports/Transformer.java
@@ -0,0 +1,587 @@
+package com.noah.timely.exports;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.noah.timely.assignment.AssignmentModel;
+import com.noah.timely.core.DataModel;
+import com.noah.timely.courses.CourseModel;
+import com.noah.timely.exam.ExamModel;
+import com.noah.timely.timetable.TimetableModel;
+import com.noah.timely.util.Constants;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+/**
+ * Transformer class implementation to tranform any data into xml representation
+ */
+class Transformer {
+
+ /**
+ * Transforms a map into xml
+ *
+ * @param type the transformation type
+ * @param map the map to be transformed
+ * @return the required xml representation of the map
+ */
+ @SuppressWarnings("unchecked")
+ public static String getXML(T data) {
+ String xmlString;
+
+ try {
+ DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
+ Document document = documentBuilder.newDocument();
+ // root element
+ Element rootElement;
+ // export main table data - data models
+ if (data.getClass() == Object[].class) {
+ Object[] ss = (Object[]) data;
+ String identifier = (String) ss[0];
+
+ List dataModelList = (List) ss[1];
+ if (dataModelList.size() == 0) return null;
+
+ rootElement = document.createElement("Table");
+ rootElement.setAttribute("name", getProperTableName(identifier));
+ document.appendChild(rootElement);
+
+ // structure and data elements
+ Element dataElement = document.createElement("TableData");
+ rootElement.appendChild(dataElement);
+ // add list nodes of dataModelList as child nodes to dataElement
+ appendTableData(identifier, document, dataElement, dataModelList);
+
+ } else if (data.getClass() == HashMap.class) {
+ // export metadata
+ Map map = (Map) data;
+ Set> entrySet = map.entrySet();
+ rootElement = document.createElement("metadata");
+ document.appendChild(rootElement);
+ // data elements
+ for (Map.Entry entry : entrySet) {
+ Element dataElement = document.createElement("data");
+ dataElement.setAttribute("name", entry.getKey());
+ dataElement.setTextContent(entry.getValue());
+ rootElement.appendChild(dataElement);
+ }
+ } else {
+ throw new IllegalArgumentException(data.getClass() + " is not supported");
+ }
+
+ // then transform generated XML to string representation
+ TransformerFactory transformerFactory = TransformerFactory.newInstance();
+ javax.xml.transform.Transformer transformer = transformerFactory.newTransformer();
+ // prettier
+ transformer.setOutputProperty(OutputKeys.METHOD, "xml");
+// transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
+// transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(3));
+ // to string result
+ StringWriter stringWriter = new StringWriter();
+ StreamResult streamResult = new StreamResult(stringWriter);
+ DOMSource domSource = new DOMSource(document);
+ transformer.transform(domSource, streamResult);
+ // results
+ xmlString = stringWriter.toString().trim();
+ } catch (ParserConfigurationException | TransformerException e) {
+ return null;
+ }
+
+ return xmlString;
+ }
+
+ /**
+ * Transforms xml data representation of data model list into list of data models
+ *
+ * @param datamodelIdentifier the identifier constant of the data contained in the xml
+ * @param xml the xml string data
+ * @return the required List of DataModels
+ */
+ public static List extends DataModel> getDataModel(String datamodelIdentifier, String xml) {
+ List extends DataModel> dataModels = new ArrayList<>();
+
+ try {
+ XmlPullParserFactory xppf = XmlPullParserFactory.newInstance();
+ XmlPullParser xpp = xppf.newPullParser();
+ xpp.setInput(new StringReader(xml));
+ // get events
+ String tableName = null, dataTagNode = null;
+ boolean isDataFound = false, isDataRead = false;
+
+ int eventType = xpp.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ final String tagName = xpp.getName();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (tagName.equals("Table")) {
+ tableName = xpp.getAttributeValue(0);
+ xpp.next();
+ }
+
+ if (tagName.equals("TableData")) {
+ isDataFound = true;
+ xpp.next();
+ }
+
+ if (haveGotDataElement(tagName)) {
+ switch (tagName) {
+ case "Assignment":
+ dataModels = readAssignmenData(xpp);
+ break;
+ case "Registered-Course":
+ dataModels = readCourseData(xpp);
+ break;
+ case "Exam":
+ dataModels = readExamData(xpp);
+ break;
+ case "Timetable":
+ case "Scheduled-Timetable":
+ dataModels = readTimetableData(xpp);
+ break;
+ default:
+ throw new IllegalStateException(dataTagNode + " is not supported");
+ }
+ }
+
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (haveGotDataElement(tagName)) {
+ dataTagNode = null;
+ }
+ }
+
+ eventType = xpp.next();
+ }
+
+ } catch (XmlPullParserException | IOException e) {
+ // debug
+ Log.e(Transformer.class.getSimpleName(), e.getMessage());
+ return null;
+ }
+
+ return dataModels;
+ }
+
+ private static boolean haveGotDataElement(String tagName) {
+ String[] dataTags = { "Assignment", "Exam", "Registered-Course", "Scheduled-Timetable", "Timetable" };
+ return Arrays.binarySearch(dataTags, tagName) >= 0;
+ }
+
+ private static List readCourseData(XmlPullParser xpp) throws XmlPullParserException, IOException {
+ List courseModels = new ArrayList<>();
+
+ CourseModel courseModel = null;
+ boolean isDataFound = false;
+ String dataTagName = "";
+ int eventType = xpp.getEventType();
+
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("Registered-Course")) {
+ // course model found
+ isDataFound = true;
+ courseModel = new CourseModel();
+ } else if (eventType == XmlPullParser.END_TAG && xpp.getName().equals("Registered-Course")) {
+ // reset flag and prepare to read another course model
+ isDataFound = false;
+ courseModels.add(courseModel);
+ } else if (isDataFound) {
+ // START_TAG event is emitted, so the next should be a TEXT event. Store the start tag's name and
+ // then prepare to read the text in-between the start and end tags
+ if (eventType == XmlPullParser.START_TAG) dataTagName = xpp.getName();
+
+ if (eventType == XmlPullParser.TEXT) {
+ // dataTagName can never be empty at this point, but to just be on a safer side, still check
+ if (!TextUtils.isEmpty(dataTagName)) {
+ switch (dataTagName) {
+ case "id":
+ courseModel.setId(Integer.parseInt(xpp.getText()));
+ break;
+ case "position":
+ courseModel.setPosition(Integer.parseInt(xpp.getText()));
+ break;
+ case "Semester":
+ courseModel.setSemester(xpp.getText());
+ break;
+ case "Credits":
+ courseModel.setCredits(Integer.parseInt(xpp.getText()));
+ break;
+ case "Course-Code":
+ courseModel.setCourseCode(xpp.getText());
+ break;
+ case "Course-Title":
+ courseModel.setCourseName(xpp.getText());
+ break;
+ }
+ }
+ }
+ }
+
+ eventType = xpp.next();
+ }
+
+ return courseModels;
+ }
+
+ private static List readAssignmenData(XmlPullParser xpp) throws XmlPullParserException, IOException {
+ List assignmentModels = new ArrayList<>();
+
+ AssignmentModel assignmentModel = null;
+ boolean isDataFound = false;
+ String dataTagName = "";
+ int eventType = xpp.getEventType();
+
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("Assignment")) {
+ // assignment model found
+ isDataFound = true;
+ assignmentModel = new AssignmentModel();
+ } else if (eventType == XmlPullParser.END_TAG && xpp.getName().equals("Assignment")) {
+ // reset flag and prepare to read another assignment model
+ isDataFound = false;
+ assignmentModels.add(assignmentModel);
+ } else if (isDataFound) {
+ // START_TAG event is emitted, so the next should be a TEXT event. Store the start tag's name and
+ // then prepare to read the text in-between the start and end tags
+ if (eventType == XmlPullParser.START_TAG) dataTagName = xpp.getName();
+
+ if (eventType == XmlPullParser.TEXT) {
+ // dataTagName can never be empty at this point, but to just be on a safer side, still check
+ if (!TextUtils.isEmpty(dataTagName)) {
+ switch (dataTagName) {
+ case "id":
+ assignmentModel.setId(Integer.parseInt(xpp.getText()));
+ break;
+ case "position":
+ assignmentModel.setPosition(Integer.parseInt(xpp.getText()));
+ break;
+ case "Submission-Status":
+ assignmentModel.setSubmitted(Boolean.parseBoolean(xpp.getText()));
+ break;
+ case "Description":
+ assignmentModel.setDescription(xpp.getText());
+ break;
+ case "Course-Code":
+ assignmentModel.setCourseCode(xpp.getText());
+ break;
+ case "Lecturer-Name":
+ assignmentModel.setLecturerName(xpp.getText());
+ break;
+ case "Submission-Date": {
+ // FIXME: 3/24/2022 merge submission date into a single instance variable
+ assignmentModel.setDate(xpp.getText());
+ assignmentModel.setSubmissionDate(xpp.getText());
+ break;
+ }
+ case "Title":
+ assignmentModel.setTitle(xpp.getText());
+ break;
+ }
+ }
+ }
+
+ }
+
+ eventType = xpp.next();
+
+ }
+
+ return assignmentModels;
+ }
+
+ private static List readExamData(XmlPullParser xpp) throws XmlPullParserException, IOException {
+ List examModels = new ArrayList<>();
+
+ ExamModel examModel = null;
+ boolean isDataFound = false;
+ String dataTagName = "";
+ int eventType = xpp.getEventType();
+
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG && xpp.getName().equals("Exam")) {
+ // exam model found
+ isDataFound = true;
+ examModel = new ExamModel();
+ } else if (eventType == XmlPullParser.END_TAG && xpp.getName().equals("Exam")) {
+ // reset flag and prepare to read another exam model
+ isDataFound = false;
+ examModels.add(examModel);
+ } else if (isDataFound) {
+ // START_TAG event is emitted, so the next should be a TEXT event. Store the start tag's name and
+ // then prepare to read the text in-between the start and end tags
+ if (eventType == XmlPullParser.START_TAG) dataTagName = xpp.getName();
+
+ if (eventType == XmlPullParser.TEXT) {
+ // dataTagName can never be empty at this point, but to just be on a safer side, still check
+ if (!TextUtils.isEmpty(dataTagName)) {
+ switch (dataTagName) {
+ case "id":
+ examModel.setId(Integer.parseInt(xpp.getText()));
+ break;
+ case "position":
+ examModel.setPosition(Integer.parseInt(xpp.getText()));
+ break;
+ case "Week":
+ examModel.setWeek(xpp.getText());
+ break;
+ case "Day":
+ examModel.setDay(xpp.getText());
+ break;
+ case "Course-Code":
+ examModel.setCourseCode(xpp.getText());
+ break;
+ case "Course-Title":
+ examModel.setCourseName(xpp.getText());
+ break;
+ case "Start-Time":
+ examModel.setStart(xpp.getText());
+ break;
+ case "End-Time":
+ examModel.setEnd(xpp.getText());
+ break;
+ }
+ }
+ }
+ }
+
+ eventType = xpp.next();
+ }
+
+ return examModels;
+ }
+
+ private static List readTimetableData(XmlPullParser xpp) throws XmlPullParserException, IOException {
+ List timetableModels = new ArrayList<>();
+
+ TimetableModel timetableModel = null;
+ boolean isDataFound = false;
+ String dataTagName = "";
+ int eventType = xpp.getEventType();
+
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG
+ && (xpp.getName().equals("Scheduled-Timetable") || xpp.getName().equals("Timetable"))) {
+ // timetable model found
+ isDataFound = true;
+ timetableModel = new TimetableModel();
+ } else if (eventType == XmlPullParser.END_TAG
+ && (xpp.getName().equals("Scheduled-Timetable") || xpp.getName().equals("Timetable"))) {
+ // reset flag and prepare to read another timetable model
+ isDataFound = false;
+ timetableModels.add(timetableModel);
+ } else if (isDataFound) {
+ // START_TAG event is emitted, so the next should be a TEXT event. Store the start tag's name and
+ // then prepare to read the text in-between the start and end tags
+ if (eventType == XmlPullParser.START_TAG) dataTagName = xpp.getName();
+
+ if (eventType == XmlPullParser.TEXT) {
+ // dataTagName can never be empty at this point, but to just be on a safer side, still check
+ if (!TextUtils.isEmpty(dataTagName)) {
+ switch (dataTagName) {
+ case "id":
+ timetableModel.setId(Integer.parseInt(xpp.getText()));
+ break;
+ case "position":
+ timetableModel.setPosition(Integer.parseInt(xpp.getText()));
+ break;
+ case "Day":
+ timetableModel.setDay(xpp.getText());
+ break;
+ case "Course-Code":
+ timetableModel.setCourseCode(xpp.getText());
+ break;
+ case "Course-Title":
+ timetableModel.setFullCourseName(xpp.getText());
+ break;
+ case "Start-Time":
+ timetableModel.setStartTime(xpp.getText());
+ break;
+ case "End-Time":
+ timetableModel.setEndTime(xpp.getText());
+ break;
+ case "Lecturer-Name":
+ timetableModel.setLecturerName(xpp.getText());
+ break;
+ case "Importance":
+ timetableModel.setImportance(xpp.getText());
+ break;
+ }
+ }
+ }
+ }
+
+ eventType = xpp.next();
+ }
+
+ return timetableModels;
+ }
+
+ private static String getProperTableName(String dataModelIdentifier) {
+ if (Constants.ASSIGNMENT.equals(dataModelIdentifier)) {
+ return "Assignments";
+ } else if (Constants.COURSE.equals(dataModelIdentifier)) {
+ return "Courses";
+ } else if (Constants.EXAM.equals(dataModelIdentifier)) {
+ return "Exams";
+ } else if (Constants.TIMETABLE.equals(dataModelIdentifier)) {
+ return "Timetable";
+ } else if (Constants.SCHEDULED_TIMETABLE.equals(dataModelIdentifier)) {
+ return "Scheduled_Timetable";
+ }
+ throw new IllegalArgumentException("The identifier " + dataModelIdentifier + " doesn't exists in database");
+
+ }
+
+ private static void appendTableData(String identifier, Document doc, Element dataElement, List data) {
+ for (DataModel dataModel : data) {
+ Element childElement = getElementFrom(identifier, doc, dataModel);
+ dataElement.appendChild(childElement);
+ }
+ }
+
+ private static Element getElementFrom(String identifier, Document doc, T datamodel) {
+ if (datamodel instanceof AssignmentModel) {
+ return getAssignmentElement(doc, (AssignmentModel) datamodel);
+ }
+ if (datamodel instanceof CourseModel) {
+ return getCourseElement(doc, (CourseModel) datamodel);
+ }
+ if (datamodel instanceof ExamModel) {
+ return getExamElement(doc, (ExamModel) datamodel);
+ }
+ if (datamodel instanceof TimetableModel) {
+ return getTimetableElement(doc, identifier, (TimetableModel) datamodel);
+ }
+ return null;
+ }
+
+ private static Element getAssignmentElement(Document document, AssignmentModel model) {
+ Element element = document.createElement("Assignment");
+ Element node1 = document.createElement("id");
+ node1.setTextContent(String.valueOf(model.getId()));
+ Element node2 = document.createElement("position");
+ node2.setTextContent(String.valueOf(model.getPosition()));
+ Element node3 = document.createElement("Submission-Status");
+ node3.setTextContent(String.valueOf(model.isSubmitted()));
+ Element node4 = document.createElement("Course-Code");
+ node4.setTextContent(model.getCourseCode());
+ Element node5 = document.createElement("Description");
+ node5.setTextContent(model.getDescription());
+ Element node6 = document.createElement("Lecturer-Name");
+ node6.setTextContent(model.getLecturerName());
+ Element node7 = document.createElement("Title");
+ node7.setTextContent(model.getTitle());
+ Element node8 = document.createElement("Submission-Date");
+ // FIXME: 3/24/2022 merge assignment date into a single instance variable
+ String submissionDate = TextUtils.isEmpty(model.getSubmissionDate()) ? model.getDate() : model.getSubmissionDate();
+ node8.setTextContent(submissionDate);
+
+ Element[] nodes = { node1, node2, node3, node4, node5, node6, node7, node8 };
+ for (Element node : nodes) element.appendChild(node);
+
+ return element;
+ }
+
+ private static Element getCourseElement(Document document, CourseModel model) {
+ Element element = document.createElement("Registered-Course");
+ Element node1 = document.createElement("id");
+ node1.setTextContent(String.valueOf(model.getId()));
+ Element node2 = document.createElement("position");
+ node2.setTextContent(String.valueOf(model.getPosition()));
+ Element node3 = document.createElement("Semester");
+ node3.setTextContent(String.valueOf(model.getSemester()));
+ Element node4 = document.createElement("Credits");
+ node4.setTextContent(String.valueOf(model.getCredits()));
+ Element node5 = document.createElement("Course-Code");
+ node5.setTextContent(model.getCourseCode());
+ Element node6 = document.createElement("Course-Title");
+ node6.setTextContent(model.getCourseName());
+
+ Element[] nodes = { node1, node2, node3, node4, node5, node6 };
+ for (Element node : nodes) element.appendChild(node);
+
+ return element;
+ }
+
+ private static Element getExamElement(Document document, ExamModel model) {
+ Element element = document.createElement("Exam");
+ Element node1 = document.createElement("id");
+ node1.setTextContent(String.valueOf(model.getId()));
+ Element node2 = document.createElement("position");
+ node2.setTextContent(String.valueOf(model.getPosition()));
+ Element node3 = document.createElement("Week");
+ node3.setTextContent(String.valueOf(model.getWeek()));
+ Element node4 = document.createElement("Course-Code");
+ node4.setTextContent(model.getCourseCode());
+ Element node5 = document.createElement("Day");
+ node5.setTextContent(model.getDay());
+ Element node6 = document.createElement("Course-Code");
+ node6.setTextContent(model.getCourseCode());
+ Element node7 = document.createElement("Course-Title");
+ node7.setTextContent(model.getCourseName());
+ Element node8 = document.createElement("Start-Time");
+ node8.setTextContent(model.getStart());
+ Element node9 = document.createElement("End-Time");
+ node9.setTextContent(model.getEnd());
+
+ Element[] nodes = { node1, node2, node3, node4, node5, node6, node7, node8, node9 };
+ for (Element node : nodes) element.appendChild(node);
+
+ return element;
+ }
+
+ private static Element getTimetableElement(Document document, String id, TimetableModel model) {
+ Element element;
+ if (id.equals(Constants.SCHEDULED_TIMETABLE))
+ element = document.createElement("Scheduled-Timetable");
+ else {
+ element = document.createElement("Timetable");
+ }
+
+ Element node1 = document.createElement("id");
+ node1.setTextContent(String.valueOf(model.getId()));
+ Element node2 = document.createElement("position");
+ node2.setTextContent(String.valueOf(model.getPosition()));
+ Element node3 = document.createElement("Course-Code");
+ node3.setTextContent(model.getCourseCode());
+ Element node4 = document.createElement("Course-Title");
+ node4.setTextContent(model.getFullCourseName());
+ Element node5 = document.createElement("Start-Time");
+ node5.setTextContent(model.getStartTime());
+ Element node6 = document.createElement("End-Time");
+ node6.setTextContent(model.getEndTime());
+ Element node7 = document.createElement("Day");
+ node7.setTextContent(model.getDay());
+ Element node8 = document.createElement("Lecturer-Name");
+ node8.setTextContent(model.getLecturerName());
+ Element node9 = document.createElement("Importance");
+ node9.setTextContent(model.getImportance());
+
+ Element[] nodes = { node1, node2, node3, node4, node5, node6, node7, node8, node9 };
+ for (Element node : nodes) element.appendChild(node);
+
+ return element;
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/noah/timely/gallery/ImageDirectory.java b/app/src/main/java/com/noah/timely/gallery/ImageDirectory.java
index 5221f1ac..ec1894c3 100644
--- a/app/src/main/java/com/noah/timely/gallery/ImageDirectory.java
+++ b/app/src/main/java/com/noah/timely/gallery/ImageDirectory.java
@@ -131,6 +131,7 @@ private void showInContextUI() {
new AlertDialog.Builder(this)
.setTitle(noticeTitle)
.setMessage(noticeMessage)
+ .setIcon(R.drawable.ic_baseline_info_24)
.setNegativeButton(cancelText, this::requestAction)
.setPositiveButton(goText, this::requestAction)
.create()
@@ -176,7 +177,7 @@ public void run() {
MediaStore.Images.Media._ID,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
MediaStore.Images.Media.SIZE,
- MediaStore.Images.Media.DISPLAY_NAME};
+ MediaStore.Images.Media.DISPLAY_NAME };
Cursor imgCursor = getApplicationContext().getContentResolver().query(storageUri, projection, null, null, null);
@@ -242,8 +243,8 @@ public ImageDirectoryRowHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
@Override
public void onBindViewHolder(@NonNull ImageDirectoryRowHolder viewHolder, int pos) {
viewHolder.with(ImageDirectory.this, imageDirectoryList, pos, accessedStorage)
- .setRequestAction(getIntent().getAction())
- .loadThumbnail();
+ .setRequestAction(getIntent().getAction())
+ .loadThumbnail();
}
@Override
diff --git a/app/src/main/java/com/noah/timely/io/IOUtils.java b/app/src/main/java/com/noah/timely/io/IOUtils.java
new file mode 100644
index 00000000..c9bc0e7a
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/io/IOUtils.java
@@ -0,0 +1,53 @@
+package com.noah.timely.io;
+
+import static com.noah.timely.io.Zipper.MAX_DATA_TRANSFER_BITS;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class IOUtils {
+
+ public static void copy(InputStream inputStream, OutputStream outputStream) throws IOException {
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ FileUtils.copy(inputStream, outputStream);
+ } else {
+ byte[] dataBuffer = new byte[MAX_DATA_TRANSFER_BITS];
+ while (inputStream.read(dataBuffer) != -1) {
+ outputStream.write(dataBuffer);
+ }
+
+ }
+ } finally {
+ inputStream.close();
+ outputStream.close();
+ }
+ }
+
+ public static String getFileExtension(File file) {
+ String ssFile = file.toString();
+ int i = ssFile.lastIndexOf('.');
+ return i > 0 ? ssFile.substring(i) : null;
+ }
+
+ public static boolean deleteTempFiles(Context context) {
+ String tempFilePath = context.getExternalFilesDir(null) + File.separator + "temp" + File.separator;
+ File tempFilesDir = new File(tempFilePath);
+
+ boolean allDeleteFlag = true;
+
+ if (tempFilesDir.exists()) {
+ for (File tempFile : tempFilesDir.listFiles()) {
+ allDeleteFlag &= tempFile.delete();
+ }
+ }
+
+ return allDeleteFlag;
+ }
+}
diff --git a/app/src/main/java/com/noah/timely/io/Zipper.java b/app/src/main/java/com/noah/timely/io/Zipper.java
new file mode 100644
index 00000000..0164c0dd
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/io/Zipper.java
@@ -0,0 +1,153 @@
+package com.noah.timely.io;
+
+import static com.noah.timely.util.AppInfoUtils.getAppName;
+import static com.noah.timely.util.AppInfoUtils.getAppVesionName;
+
+import android.content.Context;
+
+import com.noah.timely.util.Constants;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * TimeLY's native Zipper for .tmly generated files
+ */
+public class Zipper {
+
+ /**
+ * TimeLY's native export file extension
+ */
+ public static final String FILE_EXTENSION = ".tmly";
+
+ /**
+ * The file extension in which all exported data would have
+ */
+ public static final String DATA_FILE_EXTENSION = ".xml";
+
+ /**
+ * The maximum amount of data that can be transferred at once; 10MB
+ */
+ public static final int MAX_DATA_TRANSFER_BITS = 10_048_576;
+
+ /**
+ * Un-zips a .tmly file into one single folder
+ *
+ * @param context the context used in accessing application resources
+ * @param output the directory to output the zipped flle
+ * @param input the directory in which all it's contents would be zipped into one file
+ * @return true if file was zipped successfully
+ * @throws FileNotFoundException if file location specified was incorrect
+ */
+ public static Map unzipToXMLMap(Context context, String finput) throws IOException {
+ ZipInputStream zin = new ZipInputStream(new FileInputStream(finput));
+ Map xmlmap = new HashMap<>();
+
+ ZipEntry zipEntry = null;
+
+ while ((zipEntry = zin.getNextEntry()) != null) {
+ byte[] data = new byte[MAX_DATA_TRANSFER_BITS];
+ if (zin.read(data) != -1) {
+ String entryName = zipEntry.getName();
+ xmlmap.put(getDatamodelName(entryName), new String(data, Charset.forName("UTF-8")));
+ }
+
+ }
+
+ zin.close();
+ return xmlmap;
+ }
+
+ /**
+ * Zips a folder into one single file with the .tmly file extension
+ *
+ * @param context the context used in accessing application resources
+ * @param output the directory to output the zipped flle
+ * @param input the directory in which all it's contents would be zipped into one file
+ * @return true if file was zipped successfully
+ * @throws FileNotFoundException if file location specified was incorrect
+ */
+ public static boolean zipXMLMap(Context context, Map transf, String foutput) throws IOException {
+ File exportFile = new File(foutput);
+ File exportDirectory = exportFile.getParentFile();
+ boolean created = true;
+
+ if (!exportDirectory.exists()) {
+ created = exportDirectory.mkdirs();
+ }
+
+ if (!created) return false;
+ else {
+ ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(foutput));
+ zout.setComment("Archive created by " + String.format("%s v%s", getAppName(context), getAppVesionName(context)));
+
+ Set> entries = transf.entrySet();
+
+ int zippedCount = 0;
+
+ for (Map.Entry entry : entries) {
+ String filename = getProperFilename(entry.getKey());
+ ZipEntry zipEntry = new ZipEntry(filename);
+ zout.putNextEntry(zipEntry);
+
+ zout.write(entry.getValue().getBytes(Charset.forName("UTF-8")));
+ zout.closeEntry();
+ zippedCount++;
+ }
+
+ zout.finish();
+ zout.close();
+ return entries.size() == zippedCount;
+ }
+ }
+
+ private static String getProperFilename(String dataModelIdentifier) {
+ switch (dataModelIdentifier) {
+ case Constants.ASSIGNMENT:
+ return "Assignments.xml";
+ case Constants.COURSE:
+ return "Courses.xml";
+ case Constants.EXAM:
+ return "Exams.xml";
+ case Constants.TIMETABLE:
+ return "Timetable.xml";
+ case Constants.SCHEDULED_TIMETABLE:
+ return "Scheduled Timetable.xml";
+ case "Metadata":
+ return dataModelIdentifier + ".xml";
+ default:
+ throw new IllegalArgumentException("The identifier " + dataModelIdentifier + " doesn't exists in database");
+ }
+
+ }
+
+ private static String getDatamodelName(String properName) {
+ switch (properName) {
+ case "Assignments.xml":
+ return Constants.ASSIGNMENT;
+ case "Courses.xml":
+ return Constants.COURSE;
+ case "Exams.xml":
+ return Constants.EXAM;
+ case "Timetable.xml":
+ return Constants.TIMETABLE;
+ case "Scheduled Timetable.xml":
+ return Constants.SCHEDULED_TIMETABLE;
+ case "Metadata.xml":
+ return properName;
+ default:
+ throw new IllegalArgumentException("A datamodel with name: " + properName + " doesn't exist in database");
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/noah/timely/main/App.java b/app/src/main/java/com/noah/timely/main/App.java
index a27e6cf5..6c342da9 100644
--- a/app/src/main/java/com/noah/timely/main/App.java
+++ b/app/src/main/java/com/noah/timely/main/App.java
@@ -9,7 +9,6 @@
import androidx.core.content.ContextCompat;
import com.noah.timely.R;
-import com.noah.timely.util.LogUtils;
public class App extends Application {
public static final String ASSIGNMENT_CHANNEL = "TimeLY's assignments";
@@ -30,8 +29,6 @@ public void onCreate() {
}
private void createNotificationChannels() {
- LogUtils.debug(this, "Creating notification channels");
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager mgr = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
diff --git a/app/src/main/java/com/noah/timely/main/IntroPageActivity.java b/app/src/main/java/com/noah/timely/main/IntroPageActivity.java
index 7345fafe..dfb4fa05 100644
--- a/app/src/main/java/com/noah/timely/main/IntroPageActivity.java
+++ b/app/src/main/java/com/noah/timely/main/IntroPageActivity.java
@@ -19,7 +19,9 @@
import com.google.android.material.tabs.TabLayoutMediator;
import com.noah.timely.R;
-public class IntroPageActivity extends AppCompatActivity {
+public class IntroPageActivity extends AppCompatActivity implements View.OnClickListener {
+ private IntroPagerAdapter adapter;
+ private ViewPager2 pager_intro;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -27,13 +29,21 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
setContentView(R.layout.intro);
Button start = findViewById(R.id.start);
- FloatingActionButton skip = findViewById(R.id.skip);
- ViewPager2 pager_intro = findViewById(R.id.intro_pager);
- pager_intro.setAdapter(new IntroPagerAdapter(this));
+ Button skip = findViewById(R.id.skip);
+ FloatingActionButton btn_next = findViewById(R.id.next);
+ FloatingActionButton btn_prev = findViewById(R.id.prev);
+
+ adapter = new IntroPagerAdapter(this);
+ pager_intro = findViewById(R.id.intro_pager);
+ pager_intro.setAdapter(adapter);
// set up page position indicator to react to page scroll
TabLayout tab = findViewById(R.id.indicator);
new TabLayoutMediator(tab, pager_intro, (tab1, position) -> {
}).attach();
+
+ btn_prev.setOnClickListener(this);
+ btn_next.setOnClickListener(this);
+
// add scroll listener
pager_intro.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@@ -51,11 +61,13 @@ public void onPageSelected(int position) {
}
});
+
// navigate to landing page
start.setOnClickListener(this::navigateToLandingPage);
skip.setOnClickListener(this::navigateToLandingPage);
}
+
private void navigateToLandingPage(View view) {
Intent nav_main = new Intent(this, MainActivity.class);
nav_main.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
@@ -67,6 +79,18 @@ protected void onDestroy() {
super.onDestroy();
}
+ @Override
+ public void onClick(View v) {
+ int shift = 0;
+ int viewId = v.getId();
+ if (viewId == R.id.next) shift = +1;
+ else if (viewId == R.id.prev) shift = -1;
+
+ int selectedItem = pager_intro.getCurrentItem();
+ if (selectedItem >= 0 && selectedItem < adapter.getItemCount())
+ pager_intro.setCurrentItem(pager_intro.getCurrentItem() + shift);
+ }
+
private static class IntroPagerAdapter extends FragmentStateAdapter {
public IntroPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
diff --git a/app/src/main/java/com/noah/timely/main/MainActivity.java b/app/src/main/java/com/noah/timely/main/MainActivity.java
index fa3c8731..0295c3ca 100644
--- a/app/src/main/java/com/noah/timely/main/MainActivity.java
+++ b/app/src/main/java/com/noah/timely/main/MainActivity.java
@@ -5,10 +5,12 @@
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
import android.os.PowerManager;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuItem;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
@@ -33,6 +35,8 @@
import com.noah.timely.core.SchoolDatabase;
import com.noah.timely.courses.CoursesFragment;
import com.noah.timely.exam.ExamFragment;
+import com.noah.timely.exports.ImportResultsActivity;
+import com.noah.timely.exports.TMLYDataGeneratorDialog;
import com.noah.timely.scheduled.ScheduledTimetableFragment;
import com.noah.timely.settings.SettingsActivity;
import com.noah.timely.timetable.TimetableFragment;
@@ -43,7 +47,11 @@
import com.noah.timely.util.ReportActionUtil;
import com.noah.timely.util.TimelyUpdateUtils;
+import java.util.Locale;
+
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
+ private boolean dismissable;
+
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
@@ -95,6 +103,7 @@ protected void onCreate(Bundle savedInstanceState) {
new AlertDialog.Builder(this)
.setTitle(noticeTitle)
.setMessage(noticeMessage)
+ .setIcon(R.drawable.ic_baseline_info_24)
.setNegativeButton(cancelText, this::requestAction)
.setNeutralButton(neutralText, this::requestAction)
.setPositiveButton(goText, this::requestAction)
@@ -159,12 +168,23 @@ public void onBackPressed() {
if (drawer.isDrawerOpen(GravityCompat.START))
drawer.closeDrawer(GravityCompat.START);
else {
- FragmentManager manager = getSupportFragmentManager();
- FragmentTransaction transaction = manager.beginTransaction();
- Fragment fragment1 = manager.findFragmentByTag("Todo");
- if (fragment1 == null) finish();
- else super.onBackPressed();
+ if (dismissable) {
+ super.onBackPressed();
+ } else {
+ FragmentManager manager = getSupportFragmentManager();
+ Fragment fragment1 = manager.findFragmentByTag("Todo");
+ if (fragment1 != null) super.onBackPressed();
+ else {
+ String app_name = getString(R.string.app_name);
+ String exit_message = getString(R.string.exit_message);
+ String full_exit_message = String.format(Locale.US, "%s %s", exit_message, app_name);
+ Toast.makeText(this, full_exit_message, Toast.LENGTH_SHORT).show();
+ }
+ }
+ dismissable = true;
+ new Handler(getMainLooper()).postDelayed(() -> dismissable = false, 2000);
}
+
}
@Override
@@ -230,11 +250,15 @@ public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) {
new TimelyUpdateInfoDialog().show(this);
- }/* else if (menuItemId == R.id.generate) {
+ } else if (menuItemId == R.id.__export) {
+
+ new TMLYDataGeneratorDialog().show(this);
+
+ } else if (menuItemId == R.id.__import) {
- Toast.makeText(this, "No action yet", Toast.LENGTH_LONG).show();
+ startActivity(new Intent(this, ImportResultsActivity.class));
- }*/ else if (menuItemId == R.id.report) {
+ } else if (menuItemId == R.id.report) {
new AlertDialog.Builder(this)
.setTitle(R.string.report_title)
diff --git a/app/src/main/java/com/noah/timely/main/SplashScreen.java b/app/src/main/java/com/noah/timely/main/SplashScreen.java
index 1ce1f7f9..c8ce08fa 100644
--- a/app/src/main/java/com/noah/timely/main/SplashScreen.java
+++ b/app/src/main/java/com/noah/timely/main/SplashScreen.java
@@ -1,8 +1,8 @@
package com.noah.timely.main;
+import static com.noah.timely.util.AppInfoUtils.getAppVesionName;
+
import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.view.GestureDetector;
@@ -13,7 +13,6 @@
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
-import com.noah.timely.BuildConfig;
import com.noah.timely.R;
import com.noah.timely.util.PreferenceUtils;
@@ -52,14 +51,7 @@ public boolean onDoubleTap(MotionEvent e) {
//
// tv_appName.setText(wordSpan);
- String version = BuildConfig.VERSION_NAME;
- try {
- PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
- version = packageInfo.versionName;
- } catch (PackageManager.NameNotFoundException ignored) {
- }
-
- tv_version.setText(String.format("v%s", version));
+ tv_version.setText(String.format("v%s", getAppVesionName(this)));
// Load all animations
tv_appName.startAnimation(AnimationUtils.loadAnimation(this, R.anim.an_anim));
// Display main screen after the splash screen
diff --git a/app/src/main/java/com/noah/timely/scheduled/AddScheduledActivity.java b/app/src/main/java/com/noah/timely/scheduled/AddScheduledActivity.java
index 54f2cf67..90db97db 100644
--- a/app/src/main/java/com/noah/timely/scheduled/AddScheduledActivity.java
+++ b/app/src/main/java/com/noah/timely/scheduled/AddScheduledActivity.java
@@ -360,8 +360,7 @@ private void cancelTimetableNotifier(Context context, TimetableModel timetable)
.setDataAndType(Uri.parse("content://com.noah.timely.scheduled.add." + timeInMillis),
"com.noah.timely.scheduled.dataType");
- PendingIntent pi = PendingIntent.getBroadcast(context, 1156, timetableIntent,
- PendingIntent.FLAG_CANCEL_CURRENT);
+ PendingIntent pi = PendingIntent.getBroadcast(context, 1156, timetableIntent, PendingIntent.FLAG_CANCEL_CURRENT);
pi.cancel();
manager.cancel(pi);
}
diff --git a/app/src/main/java/com/noah/timely/scheduled/ScheduledTimetableFragment.java b/app/src/main/java/com/noah/timely/scheduled/ScheduledTimetableFragment.java
index 347cb2dd..f811eaa1 100644
--- a/app/src/main/java/com/noah/timely/scheduled/ScheduledTimetableFragment.java
+++ b/app/src/main/java/com/noah/timely/scheduled/ScheduledTimetableFragment.java
@@ -42,10 +42,12 @@
import com.noah.timely.core.RequestParams;
import com.noah.timely.core.RequestRunner;
import com.noah.timely.core.SchoolDatabase;
+import com.noah.timely.exports.TMLYDataGeneratorDialog;
import com.noah.timely.main.MainActivity;
import com.noah.timely.timetable.TimeTableRowHolder;
import com.noah.timely.timetable.TimetableModel;
import com.noah.timely.util.CollectionUtils;
+import com.noah.timely.util.Constants;
import com.noah.timely.util.DeviceInfoUtil;
import com.noah.timely.util.ThreadUtils;
@@ -267,7 +269,7 @@ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflat
itemCount = layout.findViewById(R.id.counter);
itemCount.setText(String.valueOf(tList.size()));
menu.findItem(R.id.select_all).setVisible(tList.isEmpty() ? false : true);
- TooltipCompat.setTooltipText(itemCount, "Classes Count");
+ TooltipCompat.setTooltipText(itemCount, getString(R.string.classes_count) + tList.size());
super.onCreateOptionsMenu(menu, inflater);
}
@@ -281,6 +283,8 @@ public void onPrepareOptionsMenu(@NonNull Menu menu) {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.select_all) {
tableRowAdapter.selectAllItems();
+ } else if (item.getItemId() == R.id.export) {
+ new TMLYDataGeneratorDialog().show(getContext(), Constants.SCHEDULED_TIMETABLE);
}
return super.onOptionsItemSelected(item);
}
diff --git a/app/src/main/java/com/noah/timely/timetable/DaysFragment.java b/app/src/main/java/com/noah/timely/timetable/DaysFragment.java
index a9ac14d1..ac3dbfcd 100644
--- a/app/src/main/java/com/noah/timely/timetable/DaysFragment.java
+++ b/app/src/main/java/com/noah/timely/timetable/DaysFragment.java
@@ -43,7 +43,9 @@
import com.noah.timely.core.RequestParams;
import com.noah.timely.core.RequestRunner;
import com.noah.timely.core.SchoolDatabase;
+import com.noah.timely.exports.TMLYDataGeneratorDialog;
import com.noah.timely.util.CollectionUtils;
+import com.noah.timely.util.Constants;
import com.noah.timely.util.DeviceInfoUtil;
import com.noah.timely.util.ThreadUtils;
@@ -212,7 +214,7 @@ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflat
itemCount = layout.findViewById(R.id.counter);
itemCount.setText(String.valueOf(tList.size()));
menu.findItem(R.id.select_all).setVisible(tList.isEmpty() ? false : true);
- TooltipCompat.setTooltipText(itemCount, "Timetable Count");
+ TooltipCompat.setTooltipText(itemCount, getString(R.string.timetable_count) + tList.size());
super.onCreateOptionsMenu(menu, inflater);
}
@@ -226,6 +228,8 @@ public void onPrepareOptionsMenu(@NonNull Menu menu) {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.select_all) {
rowAdapter.selectAllItems();
+ } else if (item.getItemId() == R.id.export) {
+ new TMLYDataGeneratorDialog().show(getContext(), Constants.TIMETABLE);
}
return super.onOptionsItemSelected(item);
}
diff --git a/app/src/main/java/com/noah/timely/timetable/TimeTableRowHolder.java b/app/src/main/java/com/noah/timely/timetable/TimeTableRowHolder.java
index 33a96386..80a2180b 100644
--- a/app/src/main/java/com/noah/timely/timetable/TimeTableRowHolder.java
+++ b/app/src/main/java/com/noah/timely/timetable/TimeTableRowHolder.java
@@ -114,7 +114,7 @@ public TimeTableRowHolder(@NonNull View rootView) {
RequestRunner.Builder builder = new RequestRunner.Builder();
builder.setOwnerContext(user.getActivity())
.setAdapterPosition(getAbsoluteAdapterPosition())
- .setPagePosition(tModel.getTimetablePosition())
+ .setPagePosition(tModel.getTimetableIndex())
.setModelList(tList)
.setTimetable(timetable);
diff --git a/app/src/main/java/com/noah/timely/timetable/TimetableModel.java b/app/src/main/java/com/noah/timely/timetable/TimetableModel.java
index 364c1096..29339ee0 100644
--- a/app/src/main/java/com/noah/timely/timetable/TimetableModel.java
+++ b/app/src/main/java/com/noah/timely/timetable/TimetableModel.java
@@ -170,7 +170,7 @@ public void setId(int id) {
this.id = id;
}
- public int getTimetablePosition() {
+ public int getTimetableIndex() {
if (this.day != null) {
switch (this.day) {
case "Monday":
diff --git a/app/src/main/java/com/noah/timely/todo/TodoListFragment.java b/app/src/main/java/com/noah/timely/todo/TodoListFragment.java
index 65d85cac..b40c6d9b 100644
--- a/app/src/main/java/com/noah/timely/todo/TodoListFragment.java
+++ b/app/src/main/java/com/noah/timely/todo/TodoListFragment.java
@@ -183,7 +183,7 @@ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflat
itemCount = layout.findViewById(R.id.counter);
itemCount.setText(String.valueOf(tdList.size()));
menu.findItem(R.id.select_all).setVisible(tdList.isEmpty() ? false : true);
- TooltipCompat.setTooltipText(itemCount, "Todo Count");
+ TooltipCompat.setTooltipText(itemCount, getString(R.string.todo_count) + tdList.size());
super.onCreateOptionsMenu(menu, inflater);
}
diff --git a/app/src/main/java/com/noah/timely/todo/TodoModel.java b/app/src/main/java/com/noah/timely/todo/TodoModel.java
index a389aa6b..bb3cab7d 100644
--- a/app/src/main/java/com/noah/timely/todo/TodoModel.java
+++ b/app/src/main/java/com/noah/timely/todo/TodoModel.java
@@ -1,7 +1,5 @@
package com.noah.timely.todo;
-import androidx.annotation.NonNull;
-
import com.noah.timely.core.DataModel;
import com.noah.timely.util.CollectionUtils;
@@ -150,8 +148,8 @@ public int getCategoryOrder() {
return searchIndex;
}
- @NonNull
@Override
+ @SuppressWarnings("all")
public String toString() {
return "TodoModel {" +
" id = " + id +
diff --git a/app/src/main/java/com/noah/timely/util/AppInfoUtils.java b/app/src/main/java/com/noah/timely/util/AppInfoUtils.java
new file mode 100644
index 00000000..af6d1945
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/util/AppInfoUtils.java
@@ -0,0 +1,47 @@
+package com.noah.timely.util;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+import com.noah.timely.BuildConfig;
+import com.noah.timely.R;
+import com.noah.timely.core.SchoolDatabase;
+
+/**
+ * Utility class to retrieve app specific information
+ */
+public class AppInfoUtils {
+
+ /**
+ * @param context the context to access app resources
+ * @return the version name of the app
+ */
+ public static String getAppVesionName(Context context) {
+ String version = BuildConfig.VERSION_NAME;
+ try {
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ version = packageInfo.versionName;
+ } catch (PackageManager.NameNotFoundException ignored) {
+ }
+
+ return version;
+ }
+
+ /**
+ * @param context the context to access app resources
+ * @return the app's current launcher label
+ */
+ public static String getAppName(Context context) {
+ return context.getString(R.string.app_name);
+ }
+
+ /**
+ * @param context the context to access app resources
+ * @return the version of the database
+ */
+ public static int getDatabaseVerion(Context context) {
+ return new SchoolDatabase(context).getDatabaseVersion();
+ }
+
+}
diff --git a/app/src/main/java/com/noah/timely/util/CollectionUtils.java b/app/src/main/java/com/noah/timely/util/CollectionUtils.java
index 5032e64f..4c621a73 100644
--- a/app/src/main/java/com/noah/timely/util/CollectionUtils.java
+++ b/app/src/main/java/com/noah/timely/util/CollectionUtils.java
@@ -1,6 +1,7 @@
package com.noah.timely.util;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Comparator;
import java.util.List;
@@ -91,4 +92,14 @@ public static int linearSearch(List target, T key) {
}
return -1; // dir was not found
}
+
+ /**
+ * Checks if a collection is empty
+ *
+ * @param collection the data to be checked
+ * @return true if it is empty or null
+ */
+ public static boolean isEmpty(Collection collection) {
+ return collection == null || collection.isEmpty();
+ }
}
diff --git a/app/src/main/java/com/noah/timely/util/Constants.java b/app/src/main/java/com/noah/timely/util/Constants.java
index 28cd9800..441309c5 100644
--- a/app/src/main/java/com/noah/timely/util/Constants.java
+++ b/app/src/main/java/com/noah/timely/util/Constants.java
@@ -108,4 +108,17 @@ public class Constants {
* Constants for Music category
*/
public static final String TODO_MUSIC = "Music_Todo";
+
+ /**
+ * Custom intent actions used in TimeLY
+ */
+ public static class ACTION {
+
+ /**
+ * Intent action to show notifications
+ */
+ public static final String SHOW_NOTIFICATION = "com.noah.timely.action.show-notification";
+
+ }
+
}
diff --git a/app/src/main/java/com/noah/timely/util/ISupplier.java b/app/src/main/java/com/noah/timely/util/ISupplier.java
new file mode 100644
index 00000000..5c492a7f
--- /dev/null
+++ b/app/src/main/java/com/noah/timely/util/ISupplier.java
@@ -0,0 +1,7 @@
+package com.noah.timely.util;
+
+@FunctionalInterface
+public interface ISupplier {
+
+ void get();
+}
diff --git a/app/src/main/java/com/noah/timely/util/PreferenceUtils.java b/app/src/main/java/com/noah/timely/util/PreferenceUtils.java
index d33f323c..f0fda388 100644
--- a/app/src/main/java/com/noah/timely/util/PreferenceUtils.java
+++ b/app/src/main/java/com/noah/timely/util/PreferenceUtils.java
@@ -93,6 +93,23 @@ public static void setStringValue(Context context, String key, String value) {
spEditor.apply();
}
+ /**
+ * Sets or create a integer preference
+ *
+ * @param context the context to be used to access the preference file
+ * @param key the preference key to be accessed
+ * @param value the value to be written
+ */
+ public static void setIntegerValue(Context context, String key, int value) {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ // preference editor
+ SharedPreferences.Editor spEditor = sharedPreferences.edit();
+ // if preference key is not created yet, create it and insert a default value
+ spEditor.putInt(key, value);
+ // apply changes
+ spEditor.apply();
+ }
+
/**
* Retrieves a string preference value
*
@@ -105,4 +122,17 @@ public static String getStringValue(Context context, String key, String defaultV
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
return sharedPreferences.getString(key, defaultValue);
}
+
+ /**
+ * Retrieves a integer preference value
+ *
+ * @param context the context to be used to access the preference file
+ * @param key the preference to be accessed
+ * @param defaultValue the default value to use, when key doesn't exist yet
+ * @return the value of the preference with (key
)
+ */
+ public static int getIntegerValue(Context context, String key, int defaultValue) {
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ return sharedPreferences.getInt(key, defaultValue);
+ }
}
diff --git a/app/src/main/java/com/noah/timely/util/TimelyUpdateUtils.java b/app/src/main/java/com/noah/timely/util/TimelyUpdateUtils.java
index 3c470218..db0b187b 100644
--- a/app/src/main/java/com/noah/timely/util/TimelyUpdateUtils.java
+++ b/app/src/main/java/com/noah/timely/util/TimelyUpdateUtils.java
@@ -98,7 +98,7 @@ private static void postNotification(Context context, String updateTitle, Update
.setSilent(true)
.setChannelId(App.GENERAL_CHANNEL_ID)
.setAutoCancel(false)
- .setSmallIcon(R.drawable.ic_n_upgrade)
+ .setSmallIcon(R.drawable.ic_baseline_info_24)
.setColor(ContextCompat.getColor(context, R.color.colorPrimary))
.setLargeIcon(icon);
}
diff --git a/app/src/main/res/anim/slide_enter.xml b/app/src/main/res/anim/slide_enter.xml
index ae593a6d..f9d47277 100644
--- a/app/src/main/res/anim/slide_enter.xml
+++ b/app/src/main/res/anim/slide_enter.xml
@@ -1,6 +1,6 @@
+ android:duration="@android:integer/config_mediumAnimTime">
diff --git a/app/src/main/res/anim/slide_out_top.xml b/app/src/main/res/anim/slide_out_top.xml
index 23ba29bf..c06c8079 100644
--- a/app/src/main/res/anim/slide_out_top.xml
+++ b/app/src/main/res/anim/slide_out_top.xml
@@ -1,6 +1,6 @@
+ android:duration="@android:integer/config_mediumAnimTime">
diff --git a/app/src/main/res/anim/slide_out_top_fade.xml b/app/src/main/res/anim/slide_out_top_fade.xml
index 1a3c7e18..316cd96d 100644
--- a/app/src/main/res/anim/slide_out_top_fade.xml
+++ b/app/src/main/res/anim/slide_out_top_fade.xml
@@ -1,6 +1,6 @@
+ android:duration="@android:integer/config_mediumAnimTime">
diff --git a/app/src/main/res/color/clr_light_text.xml b/app/src/main/res/color/clr_light_text.xml
new file mode 100644
index 00000000..4667a75b
--- /dev/null
+++ b/app/src/main/res/color/clr_light_text.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_buy_me_coffee.xml b/app/src/main/res/drawable/bg_buy_me_coffee.xml
new file mode 100644
index 00000000..5cea2eac
--- /dev/null
+++ b/app/src/main/res/drawable/bg_buy_me_coffee.xml
@@ -0,0 +1,21 @@
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_submit.xml b/app/src/main/res/drawable/bg_cta.xml
similarity index 100%
rename from app/src/main/res/drawable/bg_submit.xml
rename to app/src/main/res/drawable/bg_cta.xml
diff --git a/app/src/main/res/drawable/bg_cta_negative_rounded.xml b/app/src/main/res/drawable/bg_cta_negative_rounded.xml
new file mode 100644
index 00000000..69d45ed3
--- /dev/null
+++ b/app/src/main/res/drawable/bg_cta_negative_rounded.xml
@@ -0,0 +1,21 @@
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_cta_positive_rounded.xml b/app/src/main/res/drawable/bg_cta_positive_rounded.xml
new file mode 100644
index 00000000..67dfbe3d
--- /dev/null
+++ b/app/src/main/res/drawable/bg_cta_positive_rounded.xml
@@ -0,0 +1,21 @@
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_cta_warning_rounded.xml b/app/src/main/res/drawable/bg_cta_warning_rounded.xml
new file mode 100644
index 00000000..0b842a3a
--- /dev/null
+++ b/app/src/main/res/drawable/bg_cta_warning_rounded.xml
@@ -0,0 +1,21 @@
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_day_and_night.xml b/app/src/main/res/drawable/bg_day_and_night.xml
index 15f8976c..e97226d6 100644
--- a/app/src/main/res/drawable/bg_day_and_night.xml
+++ b/app/src/main/res/drawable/bg_day_and_night.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/btn_bg_rounded_edges.xml b/app/src/main/res/drawable/btn_bg_rounded_edges.xml
new file mode 100644
index 00000000..90b5bfff
--- /dev/null
+++ b/app/src/main/res/drawable/btn_bg_rounded_edges.xml
@@ -0,0 +1,22 @@
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/buttons_explore.xml b/app/src/main/res/drawable/buttons_explore.xml
index c99c9a72..90b5bfff 100644
--- a/app/src/main/res/drawable/buttons_explore.xml
+++ b/app/src/main/res/drawable/buttons_explore.xml
@@ -4,7 +4,7 @@
-
-
+
@@ -13,7 +13,7 @@
-
-
+
diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml
new file mode 100644
index 00000000..fa122e18
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_export.xml b/app/src/main/res/drawable/ic_baseline_export.xml
new file mode 100644
index 00000000..b0d8d05b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_export.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_import_export_24.xml b/app/src/main/res/drawable/ic_baseline_import_export_24.xml
new file mode 100644
index 00000000..44929b76
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_import_export_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_info_24.xml b/app/src/main/res/drawable/ic_baseline_info_24.xml
new file mode 100644
index 00000000..d0417708
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_info_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_save.xml b/app/src/main/res/drawable/ic_baseline_save.xml
new file mode 100644
index 00000000..f5a0bc9c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_save.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_warning_24.xml b/app/src/main/res/drawable/ic_baseline_warning_24.xml
new file mode 100644
index 00000000..dd9e0089
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_warning_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_bmc_logo.xml b/app/src/main/res/drawable/ic_bmc_logo.xml
new file mode 100644
index 00000000..15405a21
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bmc_logo.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/font/cookie.ttf b/app/src/main/res/font/cookie.ttf
new file mode 100644
index 00000000..e2e3e4c0
Binary files /dev/null and b/app/src/main/res/font/cookie.ttf differ
diff --git a/app/src/main/res/layout-v21/assignment_list_row.xml b/app/src/main/res/layout-v21/assignment_list_row.xml
index b6eacb9e..5359ffb1 100644
--- a/app/src/main/res/layout-v21/assignment_list_row.xml
+++ b/app/src/main/res/layout-v21/assignment_list_row.xml
@@ -155,6 +155,7 @@
external:layout_constraintStart_toStartOf="parent"
external:layout_constraintTop_toBottomOf="@id/buttonTopDivider"
external:srcCompat="@drawable/ic_round_check_circle"
+ android:padding="8dp"
tools:ignore="RtlSymmetry" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_add_course.xml b/app/src/main/res/layout/activity_add_course.xml
index 1aaafb2e..fa68aa45 100644
--- a/app/src/main/res/layout/activity_add_course.xml
+++ b/app/src/main/res/layout/activity_add_course.xml
@@ -152,7 +152,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_gap"
- android:background="@drawable/bg_submit"
+ android:background="@drawable/bg_cta"
android:minHeight="@dimen/button_min_height"
android:text="@string/register"
android:textColor="@android:color/white"
diff --git a/app/src/main/res/layout/activity_add_exam.xml b/app/src/main/res/layout/activity_add_exam.xml
index 86ca4357..054d9a00 100644
--- a/app/src/main/res/layout/activity_add_exam.xml
+++ b/app/src/main/res/layout/activity_add_exam.xml
@@ -139,7 +139,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_gap"
- android:background="@drawable/bg_submit"
+ android:background="@drawable/bg_cta"
android:minHeight="@dimen/button_min_height"
android:text="@string/register"
android:textColor="@android:color/white"
diff --git a/app/src/main/res/layout/activity_add_scheduled.xml b/app/src/main/res/layout/activity_add_scheduled.xml
index 7fb28b1b..fa3ef959 100644
--- a/app/src/main/res/layout/activity_add_scheduled.xml
+++ b/app/src/main/res/layout/activity_add_scheduled.xml
@@ -189,7 +189,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
- android:background="@drawable/bg_submit"
+ android:background="@drawable/bg_cta"
android:minHeight="@dimen/button_min_height"
android:text="@string/register"
android:textColor="@android:color/white"
diff --git a/app/src/main/res/layout/activity_add_timetable.xml b/app/src/main/res/layout/activity_add_timetable.xml
index 675a48e6..6d8f6720 100644
--- a/app/src/main/res/layout/activity_add_timetable.xml
+++ b/app/src/main/res/layout/activity_add_timetable.xml
@@ -138,7 +138,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
- android:background="@drawable/bg_submit"
+ android:background="@drawable/bg_cta"
android:minHeight="@dimen/button_min_height"
android:text="@string/register"
android:textColor="@android:color/white"
diff --git a/app/src/main/res/layout/activity_add_todo.xml b/app/src/main/res/layout/activity_add_todo.xml
index fb101320..990e315a 100644
--- a/app/src/main/res/layout/activity_add_todo.xml
+++ b/app/src/main/res/layout/activity_add_todo.xml
@@ -272,7 +272,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_gap"
- android:background="@drawable/bg_submit"
+ android:background="@drawable/bg_cta"
android:minHeight="@dimen/button_min_height"
android:text="@string/add_task"
android:textColor="@android:color/white"
diff --git a/app/src/main/res/layout/activity_import.xml b/app/src/main/res/layout/activity_import.xml
new file mode 100644
index 00000000..9ca01935
--- /dev/null
+++ b/app/src/main/res/layout/activity_import.xml
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/add_assignment.xml b/app/src/main/res/layout/add_assignment.xml
index 931eda56..659220f0 100644
--- a/app/src/main/res/layout/add_assignment.xml
+++ b/app/src/main/res/layout/add_assignment.xml
@@ -185,7 +185,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/layout_gap"
- android:background="@drawable/bg_submit"
+ android:background="@drawable/bg_cta"
android:minHeight="@dimen/button_min_height"
android:text="@string/register"
android:textColor="@android:color/white"
diff --git a/app/src/main/res/layout/dialog_about.xml b/app/src/main/res/layout/dialog_about.xml
index 5dbc74cd..95ffc21e 100644
--- a/app/src/main/res/layout/dialog_about.xml
+++ b/app/src/main/res/layout/dialog_about.xml
@@ -1,9 +1,8 @@
@@ -89,8 +88,8 @@
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
- external:layout_constraintTop_toBottomOf="@+id/textView4"
- tools:layout_editor_absoluteX="16dp">
+ external:layout_constraintBottom_toTopOf="@+id/bmc"
+ external:layout_constraintTop_toBottomOf="@+id/textView4">
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_add_scheduled.xml b/app/src/main/res/layout/dialog_add_scheduled.xml
index 52e01174..09434259 100644
--- a/app/src/main/res/layout/dialog_add_scheduled.xml
+++ b/app/src/main/res/layout/dialog_add_scheduled.xml
@@ -117,8 +117,10 @@
android:layout_marginLeft="4dp"
android:layout_marginTop="16dp"
android:layout_weight="5"
+ android:background="@drawable/bg_form"
android:drawablePadding="8dp"
android:maxLines="1"
+ android:hint="@string/end_time_hint"
external:drawableEndCompat="@drawable/ic_access_time"
external:drawableRightCompat="@drawable/ic_access_time"
tools:ignore="Autofill,TextFields" />
diff --git a/app/src/main/res/layout/dialog_export_success.xml b/app/src/main/res/layout/dialog_export_success.xml
new file mode 100644
index 00000000..7d496b97
--- /dev/null
+++ b/app/src/main/res/layout/dialog_export_success.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_generate.xml b/app/src/main/res/layout/dialog_generate.xml
new file mode 100644
index 00000000..b77a4465
--- /dev/null
+++ b/app/src/main/res/layout/dialog_generate.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_import_success.xml b/app/src/main/res/layout/dialog_import_success.xml
new file mode 100644
index 00000000..36fcac80
--- /dev/null
+++ b/app/src/main/res/layout/dialog_import_success.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_processing.xml b/app/src/main/res/layout/dialog_processing.xml
new file mode 100644
index 00000000..6127d462
--- /dev/null
+++ b/app/src/main/res/layout/dialog_processing.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_todo.xml b/app/src/main/res/layout/fragment_todo.xml
index 13443f3b..a67ad941 100644
--- a/app/src/main/res/layout/fragment_todo.xml
+++ b/app/src/main/res/layout/fragment_todo.xml
@@ -593,6 +593,7 @@
android:indeterminate="true"
external:layout_constraintBottom_toBottomOf="parent"
external:layout_constraintEnd_toEndOf="parent"
+ external:layout_constraintVertical_bias="0.45"
external:layout_constraintStart_toStartOf="parent"
external:layout_constraintTop_toTopOf="parent"
external:mpb_progressStyle="circular" />
@@ -604,7 +605,7 @@
android:layout_marginTop="@dimen/layout_gap"
android:text="@string/category_load"
android:textColor="@android:color/black"
- android:textSize="18sp"
+ android:textSize="@dimen/loader_text_size"
external:layout_constraintEnd_toEndOf="parent"
external:layout_constraintStart_toStartOf="parent"
external:layout_constraintTop_toBottomOf="@+id/indeterminateProgress" />
diff --git a/app/src/main/res/layout/import_list_row.xml b/app/src/main/res/layout/import_list_row.xml
new file mode 100644
index 00000000..e08c3f76
--- /dev/null
+++ b/app/src/main/res/layout/import_list_row.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/intro.xml b/app/src/main/res/layout/intro.xml
index 7aeb867c..7818600b 100644
--- a/app/src/main/res/layout/intro.xml
+++ b/app/src/main/res/layout/intro.xml
@@ -43,19 +43,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+ external:layout_constraintStart_toStartOf="parent"
+ tools:visibility="gone" />
+
+
+
+
+ external:maxImageSize="@dimen/fab_icon_size"
+ external:srcCompat="@drawable/ic_arrow_forward" />
\ No newline at end of file
diff --git a/app/src/main/res/menu/ee_nav_menu.xml b/app/src/main/res/menu/ee_nav_menu.xml
index ab0c5617..8de0f591 100644
--- a/app/src/main/res/menu/ee_nav_menu.xml
+++ b/app/src/main/res/menu/ee_nav_menu.xml
@@ -79,10 +79,15 @@
android:icon="@drawable/ic_whats_new"
android:title="@string/whats_new" />
-
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/list_menu_courses.xml b/app/src/main/res/menu/list_menu_courses.xml
index 2be6aea8..4af4752d 100644
--- a/app/src/main/res/menu/list_menu_courses.xml
+++ b/app/src/main/res/menu/list_menu_courses.xml
@@ -12,7 +12,12 @@
+
+
diff --git a/app/src/main/res/menu/list_menu_exams.xml b/app/src/main/res/menu/list_menu_exams.xml
index 76a2e87c..852ca33d 100644
--- a/app/src/main/res/menu/list_menu_exams.xml
+++ b/app/src/main/res/menu/list_menu_exams.xml
@@ -12,7 +12,12 @@
+
+
diff --git a/app/src/main/res/menu/list_menu_image.xml b/app/src/main/res/menu/list_menu_image.xml
index 0b095ccd..a3615975 100644
--- a/app/src/main/res/menu/list_menu_image.xml
+++ b/app/src/main/res/menu/list_menu_image.xml
@@ -5,7 +5,7 @@
\ No newline at end of file
diff --git a/app/src/main/res/menu/list_menu_scheduled.xml b/app/src/main/res/menu/list_menu_scheduled.xml
index 151234d5..423341fa 100644
--- a/app/src/main/res/menu/list_menu_scheduled.xml
+++ b/app/src/main/res/menu/list_menu_scheduled.xml
@@ -12,7 +12,12 @@
+
+
diff --git a/app/src/main/res/menu/list_menu_timetable.xml b/app/src/main/res/menu/list_menu_timetable.xml
index eb93a392..4497ea1f 100644
--- a/app/src/main/res/menu/list_menu_timetable.xml
+++ b/app/src/main/res/menu/list_menu_timetable.xml
@@ -12,7 +12,12 @@
+
+
diff --git a/app/src/main/res/menu/list_menu_todo.xml b/app/src/main/res/menu/list_menu_todo.xml
index 86aa2a20..49ea1ee4 100644
--- a/app/src/main/res/menu/list_menu_todo.xml
+++ b/app/src/main/res/menu/list_menu_todo.xml
@@ -12,7 +12,7 @@
diff --git a/app/src/main/res/menu/nav_menu.xml b/app/src/main/res/menu/nav_menu.xml
index 6a2e6bce..cd95a3ab 100644
--- a/app/src/main/res/menu/nav_menu.xml
+++ b/app/src/main/res/menu/nav_menu.xml
@@ -74,10 +74,15 @@
android:icon="@drawable/ic_whats_new"
android:title="@string/whats_new" />
-
+
+
+
- 360dp
370dp
38sp
- 16sp
+ 18sp
70sp
40sp
24sp
@@ -65,7 +65,7 @@
300dp
82dp
10dp
- 120dp
+ 140dp
235dp
130dp
130dp
@@ -83,4 +83,12 @@
20sp
180dp
14sp
+ 400dp
+ 20sp
+ 24sp
+ 20sp
+ 300dp
+ 270dp
+ 18sp
+ 18sp
\ No newline at end of file
diff --git a/app/src/main/res/values-night/dimens.xml b/app/src/main/res/values-night/dimens.xml
new file mode 100644
index 00000000..55344e51
--- /dev/null
+++ b/app/src/main/res/values-night/dimens.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values-night/themes.xml
similarity index 92%
rename from app/src/main/res/values/styles.xml
rename to app/src/main/res/values-night/themes.xml
index 39b1b8cd..e7612b44 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,5 +1,5 @@
-
+
@@ -56,8 +54,7 @@
+
+
diff --git a/app/src/main/res/values-notnight/dimens.xml b/app/src/main/res/values-notnight/dimens.xml
new file mode 100644
index 00000000..55344e51
--- /dev/null
+++ b/app/src/main/res/values-notnight/dimens.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-sw320dp/dimens.xml b/app/src/main/res/values-sw320dp/dimens.xml
index b93ed2cb..4d1b5664 100644
--- a/app/src/main/res/values-sw320dp/dimens.xml
+++ b/app/src/main/res/values-sw320dp/dimens.xml
@@ -82,4 +82,12 @@
18sp
140dp
12sp
+ 340dp
+ 18sp
+ 18sp
+ 16sp
+ 250dp
+ 210dp
+ 14sp
+ 16sp
\ No newline at end of file
diff --git a/app/src/main/res/values-sw360dp/dimens.xml b/app/src/main/res/values-sw360dp/dimens.xml
index 265cfb76..bff4b2e3 100644
--- a/app/src/main/res/values-sw360dp/dimens.xml
+++ b/app/src/main/res/values-sw360dp/dimens.xml
@@ -17,7 +17,7 @@
300dp
340dp
38sp
- 18sp
+ 16sp
56sp
30sp
18sp
@@ -47,7 +47,7 @@
14sp
14sp
70dp
- 220dp
+ 210dp
14sp
16sp
180dp
@@ -83,4 +83,12 @@
20sp
180dp
14sp
+ 340dp
+ 18sp
+ 20sp
+ 18sp
+ 250dp
+ 230dp
+ 18sp
+ 16sp
\ No newline at end of file
diff --git a/app/src/main/res/values-sw411dp/dimens.xml b/app/src/main/res/values-sw411dp/dimens.xml
index bf27f79e..97ad308e 100644
--- a/app/src/main/res/values-sw411dp/dimens.xml
+++ b/app/src/main/res/values-sw411dp/dimens.xml
@@ -83,4 +83,12 @@
20sp
180dp
14sp
+ 420dp
+ 20sp
+ 24sp
+ 20sp
+ 300dp
+ 270dp
+ 18sp
+ 18sp
\ No newline at end of file
diff --git a/app/src/main/res/values-sw440dp/dimens.xml b/app/src/main/res/values-sw440dp/dimens.xml
index 221378dd..3bfd1d2d 100644
--- a/app/src/main/res/values-sw440dp/dimens.xml
+++ b/app/src/main/res/values-sw440dp/dimens.xml
@@ -83,4 +83,12 @@
20sp
180dp
14sp
+ 420dp
+ 20sp
+ 24sp
+ 20sp
+ 300dp
+ 270dp
+ 18sp
+ 18sp
\ No newline at end of file
diff --git a/app/src/main/res/values-sw480dp/dimens.xml b/app/src/main/res/values-sw480dp/dimens.xml
index d41ceea2..94c620bd 100644
--- a/app/src/main/res/values-sw480dp/dimens.xml
+++ b/app/src/main/res/values-sw480dp/dimens.xml
@@ -83,4 +83,12 @@
20sp
180dp
14sp
+ 420dp
+ 20sp
+ 24sp
+ 20sp
+ 300dp
+ 270dp
+ 18sp
+ 18sp
\ No newline at end of file
diff --git a/app/src/main/res/values-sw533dp/dimens.xml b/app/src/main/res/values-sw533dp/dimens.xml
index 75514134..c7a0e4ef 100644
--- a/app/src/main/res/values-sw533dp/dimens.xml
+++ b/app/src/main/res/values-sw533dp/dimens.xml
@@ -83,4 +83,12 @@
20sp
180dp
14sp
+ 420dp
+ 20sp
+ 24sp
+ 20sp
+ 300dp
+ 270dp
+ 18sp
+ 18sp
\ No newline at end of file
diff --git a/app/src/main/res/values-sw560dp/dimens.xml b/app/src/main/res/values-sw560dp/dimens.xml
index 4e5df735..f4c7dc38 100644
--- a/app/src/main/res/values-sw560dp/dimens.xml
+++ b/app/src/main/res/values-sw560dp/dimens.xml
@@ -83,4 +83,12 @@
20sp
180dp
14sp
+ 420dp
+ 20sp
+ 24sp
+ 20sp
+ 300dp
+ 270dp
+ 18sp
+ 18sp
\ No newline at end of file
diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml
index 75514134..c7a0e4ef 100644
--- a/app/src/main/res/values-sw600dp/dimens.xml
+++ b/app/src/main/res/values-sw600dp/dimens.xml
@@ -83,4 +83,12 @@
20sp
180dp
14sp
+ 420dp
+ 20sp
+ 24sp
+ 20sp
+ 300dp
+ 270dp
+ 18sp
+ 18sp
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 3e674b01..f48bbeee 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -6,7 +6,7 @@
#277FFE
#EDF3FF
@color/accent
- #FE0000
+ #DF0505
#502E8D
#F06292
#FF2424
@@ -40,11 +40,20 @@
#0D19629C
#40164D79
#0DFF8800
+ #1AFF8800
#0DFF4444
+ #1AFF4444
#0D669900
+ #1A669900
#0DAA66CC
+ #1AAA66CC
#0D0099CC
+ #1A0099CC
#0D018786
+ #1A018786
#0DFF6347
+ #1AFF6347
+ #F0F4F6
+ #ECECEC
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index b7d5e95d..61bee667 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -83,4 +83,12 @@
180dp
12sp
48dp
+ 340dp
+ 18sp
+ 18sp
+ 16sp
+ 250dp
+ 210dp
+ 14sp
+ 16sp
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0129e636..b8fc716b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,5 +1,6 @@
+ Swipe or Tap again to exit
TimeLY
Settings
School
@@ -92,7 +93,8 @@
No Images Attached
Image
add course
- Generate and Share
+ Export
+ Import
Check for updates
Good Morning
Good Afternoon
@@ -117,6 +119,7 @@
Yes
v1.0.0
Use dialogs to add new task and reminders. This won\'t work for smaller screen sizes
+ Use dialogs to show data to be exported/imported. This won\'t work for smaller screen sizes
Add timetable
add exams
Good Night
@@ -185,5 +188,34 @@
Time range not set
Task viewer
Other kinds of notifications
- Selct all
+ Select all
+ Buy me a coffee
+ Choose data to export
+ Data exported successfully
+ Data imported Successfully
+ share
+ close
+ skip
+ Export
+ Processing …
+ Import from file
+ Locate
+ Import from file
+ Click on the Import from file button to import data
+ Initializing, please wait …
+ Sharing file using
+ Share using
+ Nothing to send
+ Open folder
+ Open link using
+ Assignment Count:\
+ Courses Count:\
+ Exams Count:\
+ Classes Count:\
+ Timetable Count:\
+ Todo Count:\
+ Scheduled Classes
+ 24
+ Select file using
+ Import Selected
\ No newline at end of file
diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values/themes.xml
similarity index 94%
rename from app/src/main/res/values-night/styles.xml
rename to app/src/main/res/values/themes.xml
index 39b1b8cd..aed4f3ef 100644
--- a/app/src/main/res/values-night/styles.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,5 +1,5 @@
-
+
@@ -57,7 +56,6 @@
+
+
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index f96a7fe6..3d9582b3 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -39,7 +39,7 @@
external:icon="@drawable/ic_baseline_build_24"
external:key="prefer_dialog"
external:summary="@string/dialog_pref"
- external:title="Prefer dialogs" />
+ external:title="Prefer dialogs for input" />
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/release_note.xml b/app/src/main/res/xml/release_note.xml
index e736e505..67920ea6 100644
--- a/app/src/main/res/xml/release_note.xml
+++ b/app/src/main/res/xml/release_note.xml
@@ -1,9 +1,24 @@
- Fixed bugs in Todo list.
+ Assignment list items now displays full submission status.
- Added select-all feature
+ Changed status bar color to reflect current theme.
+
+
+ Added, Click-to-view full list item details on some lists.
+
+
+ List-count tooltip displays size of list.
+
+
+ Added Export/Import data feature.
+
+
+ Improved Introduction Page; added skip and improved naviagation.
+
+
+ Working on full support for Android 12 and 13.
diff --git a/build.gradle b/build.gradle
index a3380270..2371805c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,6 +3,7 @@
buildscript {
repositories {
google()
+ //noinspection GrDeprecatedAPIUsage
jcenter()
mavenCentral()
maven { url 'https://maven.google.com' }
@@ -10,7 +11,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:7.0.1'
+ classpath 'com.android.tools.build:gradle:7.1.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -25,6 +26,7 @@ allprojects {
repositories {
google()
+ //noinspection GrDeprecatedAPIUsage
jcenter()
maven { url "https://oss.jfrog.org/libs-snapshot" }
maven { url "https://www.jitpack.io" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index a2086c59..f2c6c42e 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
diff --git a/lib/src/main/java/com/tools/lib/FunctionalityTest.java b/lib/src/main/java/com/tools/lib/FunctionalityTest.java
index 4847bf06..26fea7de 100644
--- a/lib/src/main/java/com/tools/lib/FunctionalityTest.java
+++ b/lib/src/main/java/com/tools/lib/FunctionalityTest.java
@@ -1,13 +1,16 @@
package com.tools.lib;
+import java.util.Arrays;
+
@SuppressWarnings("all")
public class FunctionalityTest {
- private static void setError(String error) {
- System.out.println("Error: " + error);
- }
-
public static void main(String... args) {
+ System.out.println(haveGotDataElement("Registered-Course"));
+ }
+ private static boolean haveGotDataElement(String tagName) {
+ String[] dataTags = { "Assignment", "Exam", "Registered-Course", "Scheduled-Timetable", "Timetable" };
+ return Arrays.binarySearch(dataTags, tagName) >= 0;
}
-}
+}
\ No newline at end of file
diff --git a/lib/src/main/java/com/tools/lib/VectorDrawableToSVG.java b/lib/src/main/java/com/tools/lib/VectorDrawableToSVG.java
index f64d707a..ee3956f5 100644
--- a/lib/src/main/java/com/tools/lib/VectorDrawableToSVG.java
+++ b/lib/src/main/java/com/tools/lib/VectorDrawableToSVG.java
@@ -7,8 +7,8 @@
public class VectorDrawableToSVG {
private static final String filePath
- = "C:\\Users\\Noah\\StudioProjects\\TimeLY\\app\\src\\main\\res\\drawable\\wavy.xml";
- private static final String storagePath = "C:\\Users\\Noah\\Desktop\\wavy.svg";
+ = "C:\\Users\\Noah\\StudioProjects\\TimeLY\\app\\src\\main\\res\\drawable\\ic_arrow_down.xml";
+ private static final String storagePath = "C:\\Users\\Noah\\Desktop\\ic_arrow_down.svg";
public static void main(String[] args) {
startConversion();
diff --git a/lib/src/main/java/com/tools/lib/XMLDocumentTest.java b/lib/src/main/java/com/tools/lib/XMLDocumentTest.java
new file mode 100644
index 00000000..755844f8
--- /dev/null
+++ b/lib/src/main/java/com/tools/lib/XMLDocumentTest.java
@@ -0,0 +1,57 @@
+package com.tools.lib;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.io.StringWriter;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+public class XMLDocumentTest {
+
+ public static void main(String... args) {
+ try {
+ DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
+ Document document = documentBuilder.newDocument();
+ // root element
+ Element rootElement = document.createElement("Table");
+ document.appendChild(rootElement);
+ // structure element
+ Element structureElement = document.createElement("TableStructure");
+ rootElement.appendChild(structureElement);
+ // table name attribute
+ Attr nameAttr = document.createAttribute("name");
+ nameAttr.setValue("Timetable");
+ rootElement.setAttributeNode(nameAttr);
+ // then transform generated XML to string representation
+ TransformerFactory transformerFactory = TransformerFactory.newInstance();
+ Transformer transformer = transformerFactory.newTransformer();
+ // prettier
+ transformer.setOutputProperty(OutputKeys.METHOD, "xml");
+ transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ transformer.setOutputProperty(OutputKeys.STANDALONE, "yes");
+ transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", String.valueOf(3));
+ // to string result
+ StringWriter stringWriter = new StringWriter();
+ StreamResult streamResult = new StreamResult(stringWriter);
+ DOMSource domSource = new DOMSource(document);
+ transformer.transform(domSource, streamResult);
+ // results
+ String xmlString = stringWriter.toString().trim();
+ System.out.println(xmlString);
+ } catch (ParserConfigurationException | TransformerException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/lib/src/main/java/com/tools/lib/ZipTest.java b/lib/src/main/java/com/tools/lib/ZipTest.java
new file mode 100644
index 00000000..2bff1f1a
--- /dev/null
+++ b/lib/src/main/java/com/tools/lib/ZipTest.java
@@ -0,0 +1,115 @@
+package com.tools.lib;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+public class ZipTest {
+
+ /**
+ * TimeLY's native export file extension
+ */
+ public static final String FILE_EXTENSION = ".tmly";
+
+ /**
+ * The file extension in which all exported data would have
+ */
+ public static final String DATA_FILE_EXTENSION = ".txt";
+
+ public static void main(String... args) {
+ /////////////////////////////////////////////////////////////////////////////////
+ Map stringMap = null;
+ try {
+ stringMap = unzipToXMLArray("C:\\Users\\Noah\\Desktop\\test.zip");
+ } catch (IOException e) {
+ System.out.println("File unzip failed, file not unipped: " + e.getMessage());
+ }
+
+ if (stringMap != null) {
+ System.out.println("File unzipped succesfully\n\n");
+ for (Map.Entry entry : stringMap.entrySet()) {
+ System.out.println(entry.getKey().trim() + "=" + entry.getValue().trim());
+ }
+ } else System.out.println("File unzip failed, file not unipped 2");
+ }
+
+ /**
+ * Zips a folder into one single file with the .tmly file extension
+ *
+ * @param context the context used in accessing application resources
+ * @param output the directory to output the zipped flle
+ * @param input the directory in which all it's contents would be zipped into one file
+ * @return true if file was zipped successfully
+ * @throws FileNotFoundException if file location specified was incorrect
+ */
+ public static boolean zipXMLArray(Map transf, String foutput) throws IOException {
+ File exportFile = new File(foutput);
+ File exportDirectory = exportFile.getParentFile();
+ if (!exportDirectory.exists()) {
+ boolean created = exportDirectory.mkdirs();
+ }
+
+ ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(foutput));
+ zout.setComment("Archive created by " + String.format("%s v%s", "TimeLY", "1.2.0"));
+
+ Set> entries = transf.entrySet();
+
+ int zippedCount = 0;
+
+ for (Map.Entry entry : entries) {
+ String filename = entry.getKey();
+ ZipEntry zipEntry = new ZipEntry(filename + ".txt");
+ zipEntry.setSize(entry.getValue().length());
+ zout.putNextEntry(zipEntry);
+
+ zout.write(entry.getValue().getBytes());
+ zout.closeEntry();
+ zippedCount++;
+ }
+
+ zout.finish();
+ zout.close();
+
+ return entries.size() == zippedCount;
+ }
+
+ /**
+ * Un-zips a .tmly file into one single folder
+ *
+ * @param context the context used in accessing application resources
+ * @param output the directory to output the zipped flle
+ * @param input the directory in which all it's contents would be zipped into one file
+ * @return true if file was zipped successfully
+ * @throws FileNotFoundException if file location specified was incorrect
+ */
+ public static Map unzipToXMLArray(String finput) throws IOException {
+ ZipInputStream zin = new ZipInputStream(new FileInputStream(finput));
+ Map xmlmap = new HashMap<>();
+
+ ZipEntry zipEntry = null;
+
+ while ((zipEntry = zin.getNextEntry()) != null) {
+ byte[] data = new byte[10_048_576];
+ if (zin.read(data) != -1) {
+ String entryName = zipEntry.getName();
+ entryName = entryName.substring(0, entryName.indexOf(DATA_FILE_EXTENSION));
+ System.out.println("Unzipped: " + entryName);
+ xmlmap.put(entryName, new String(data, Charset.forName("UTF-8")));
+ }
+
+ }
+
+ zin.close();
+ return xmlmap;
+ }
+
+}