From 535359bf4f596a1ec6c9f5e6a05d76208827705d Mon Sep 17 00:00:00 2001 From: Niko Strijbol Date: Thu, 26 Oct 2017 00:56:55 +0200 Subject: [PATCH 1/2] Add prefix options and improve duplicate detection --- .../minerva/AgendaDuplicateDetector.java | 54 +++++----- .../sync/minerva/helpers/CalendarSync.java | 100 ++++++++++++++++-- .../hydra/ui/preferences/MinervaFragment.java | 31 +++++- .../ugent/zeus/hydra/utils/StringUtils.java | 30 ++++++ app/src/main/res/values/strings.xml | 8 +- app/src/main/res/xml/pref_minerva.xml | 13 +++ 6 files changed, 201 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/be/ugent/zeus/hydra/data/network/requests/minerva/AgendaDuplicateDetector.java b/app/src/main/java/be/ugent/zeus/hydra/data/network/requests/minerva/AgendaDuplicateDetector.java index 7929ce0cd..f0a2c9837 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/data/network/requests/minerva/AgendaDuplicateDetector.java +++ b/app/src/main/java/be/ugent/zeus/hydra/data/network/requests/minerva/AgendaDuplicateDetector.java @@ -5,16 +5,14 @@ import be.ugent.zeus.hydra.data.models.minerva.Agenda; import be.ugent.zeus.hydra.data.models.minerva.AgendaItem; import be.ugent.zeus.hydra.data.models.minerva.Course; +import java8.lang.Iterables; import java8.util.Objects; import java8.util.function.Function; import java8.util.stream.Collectors; import java8.util.stream.StreamSupport; import org.threeten.bp.ZonedDateTime; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Attempts to filter duplicates from a list of calendar items. @@ -23,11 +21,18 @@ */ public class AgendaDuplicateDetector implements Function { + /** + * These are edit modes we do not want to show to the user. + */ + private static final Set HIDDEN_TYPES = new HashSet<>(Collections.singletonList("set_invisible")); + @Override public Agenda apply(Agenda agenda) { List agendaItems = agenda.getItems(); + Iterables.removeIf(agendaItems, item -> HIDDEN_TYPES.contains(item.getLastEditType())); + // We first categorize the items per course. Map> mapped = StreamSupport.stream(agendaItems) .collect(Collectors.groupingBy(AgendaItem::getCourse)); @@ -100,7 +105,6 @@ private List filterDuplicates(Course course, List items) } // If there are none left, we add them all, since we don't know which one you want. - assert noMoreOasis.isEmpty(); finalItems.addAll(mergeLocations(endList)); } } @@ -109,33 +113,31 @@ private List filterDuplicates(Course course, List items) } private List mergeLocations(List items) { - // Check if they are all the same - boolean isSame = true; - AgendaItem last = items.get(0); - for (AgendaItem item : items.subList(1, items.size())) { - // We check the title and description. We already know other things, such as the - // dates are the same. - if (!TextUtils.equals(item.getTitle(), last.getTitle()) || !TextUtils.equals(item.getContent(), item.getContent())) { - isSame = false; - break; - } - } - if (isSame) { - // Merge them into one, with an adjusted location. We merge into the first one. - // TODO: better joining + List finalItems = new ArrayList<>(); + + // We currently consider two events the same if their titles are the same. Group the events by title. + Map> perTitle = StreamSupport.stream(items) + .collect(Collectors.groupingBy(AgendaItem::getTitle)); - String[] locations = StreamSupport.stream(items) + for (List item : perTitle.values()) { + if (item.size() == 1) { + finalItems.add(item.get(0)); + continue; + } + // Get the first one. + AgendaItem first = items.get(0); + // Merge the locations. + String[] locations = StreamSupport.stream(item) .map(AgendaItem::getLocation) .filter(Objects::nonNull) .distinct() .toArray(String[]::new); - last.setMerged(true); - last.setLocation(TextUtils.join("\n", locations)); - return Collections.singletonList(last); - } else { - // Else we just add them all, since they differ in ways we don't support yet. - return items; + first.setLocation(TextUtils.join("\n", locations)); + first.setMerged(true); + finalItems.add(first); } + + return finalItems; } } diff --git a/app/src/main/java/be/ugent/zeus/hydra/data/sync/minerva/helpers/CalendarSync.java b/app/src/main/java/be/ugent/zeus/hydra/data/sync/minerva/helpers/CalendarSync.java index e36e6bb36..d4786eb41 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/data/sync/minerva/helpers/CalendarSync.java +++ b/app/src/main/java/be/ugent/zeus/hydra/data/sync/minerva/helpers/CalendarSync.java @@ -13,6 +13,7 @@ import android.provider.CalendarContract; import android.support.v4.app.NotificationCompat; import android.support.v4.content.ContextCompat; +import android.text.TextUtils; import android.util.Log; import be.ugent.zeus.hydra.BuildConfig; @@ -31,6 +32,7 @@ import be.ugent.zeus.hydra.repository.requests.Result; import be.ugent.zeus.hydra.ui.minerva.CalendarPermissionActivity; import be.ugent.zeus.hydra.ui.preferences.MinervaFragment; +import be.ugent.zeus.hydra.utils.StringUtils; import java8.util.Maps; import java8.util.function.Functions; import java8.util.stream.Collectors; @@ -51,13 +53,12 @@ public class CalendarSync { private static final String TAG = "CalendarSync"; + private static final String FIRST_SYNC_BUILT_IN_CALENDAR = "once_first_calendar"; private static long NO_CALENDAR = -1; private final AgendaDao calendarDao; private final CourseDao courseDao; private final Context context; - private static final String FIRST_SYNC_BUILT_IN_CALENDAR = "once_first_calendar"; - public CalendarSync(AgendaDao calendarDao, CourseDao courseDao, Context context) { this.calendarDao = calendarDao; this.courseDao = courseDao; @@ -245,6 +246,11 @@ private void synchronizeCalendar(Account account, boolean isInitialSync, Synchro } } + // Sometimes our database loses events, or the user selects another option causing + // the device calendar to contain things that are no longer in our calendar. We cannot know the id of those, + // so we get all ids, remove the items we still know about and remove the rest. + Set allDeviceIds = getAllIdsFromDeviceCalendar(account, resolver); + // Update Calendar items, as they might have changed. for (AgendaItem updatedItem : diff.getUpdated()) { long itemCalendarId = updatedItem.getCalendarId(); @@ -256,9 +262,16 @@ private void synchronizeCalendar(Account account, boolean isInitialSync, Synchro } else { // Update the item. Uri itemUri = ContentUris.withAppendedId(uri, itemCalendarId); resolver.update(itemUri, values, null, null); + allDeviceIds.remove(itemCalendarId); } } + // Remove the events we lost from the calendar. + for (Long id : allDeviceIds) { + Uri itemUri = ContentUris.withAppendedId(uri, id); + resolver.delete(itemUri, null, null); + } + // Add new items to the calendar. for (AgendaItem newItem : diff.getNew()) { ContentValues values = toCalendarValues(calendarId, newItem); @@ -339,13 +352,39 @@ private int deleteCalendarFor(Account account, ContentResolver resolver) { String[] selArgs = new String[]{account.name, MinervaConfig.ACCOUNT_TYPE}; - int rows = resolver.delete( + return resolver.delete( CalendarContract.Calendars.CONTENT_URI, selection, selArgs ); + } + + private Set getAllIdsFromDeviceCalendar(Account account, ContentResolver resolver) { + + String[] projection = {CalendarContract.Events._ID}; - return rows; + String selection = + CalendarContract.Calendars.ACCOUNT_NAME + + " = ? AND " + + CalendarContract.Calendars.ACCOUNT_TYPE + + " = ? "; + + String[] selArgs = new String[]{account.name, MinervaConfig.ACCOUNT_TYPE}; + + Cursor result = resolver.query(CalendarContract.Calendars.CONTENT_URI, projection, selection, selArgs, null); + if (result == null) { + return new HashSet<>(); + } + + try { + Set resultSet = new HashSet<>(); + while (result.moveToNext()) { + resultSet.add(result.getLong(result.getColumnIndexOrThrow(CalendarContract.Events._ID))); + } + return resultSet; + } finally { + result.close(); + } } /** @@ -398,8 +437,8 @@ private int getCalendarColour() { private ContentValues toCalendarValues(long calendarId, AgendaItem item) { ContentValues contentValues = new ContentValues(); contentValues.put(CalendarContract.Events.CALENDAR_ID, calendarId); - contentValues.put(CalendarContract.Events.TITLE, item.getTitle()); - contentValues.put(CalendarContract.Events.DESCRIPTION, item.getContent()); + contentValues.put(CalendarContract.Events.TITLE, getTitleFor(item)); + contentValues.put(CalendarContract.Events.DESCRIPTION, getDescriptionFor(item)); contentValues.put(CalendarContract.Events.DTSTART, item.getStartDate().toInstant().toEpochMilli()); contentValues.put(CalendarContract.Events.DTEND, item.getEndDate().toInstant().toEpochMilli()); // Convert Java 8 TimeZone to old TimeZone @@ -412,4 +451,53 @@ private ContentValues toCalendarValues(long calendarId, AgendaItem item) { contentValues.put(CalendarContract.Events.CUSTOM_APP_URI, item.getUri()); return contentValues; } + + /** + * Get the title for an event in the calendar, intended for the device calendar. This method will consider the user + * preferences in regards to prefixing with the course's name and using acronyms or not. + * + * @param item The item to get the title for. + * + * @return The title. + */ + private String getTitleFor(AgendaItem item) { + String title; + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + if (preferences.getBoolean(MinervaFragment.PREF_PREFIX_EVENT_TITLES, false) + && !TextUtils.equals(item.getTitle(), item.getCourse().getTitle())) { + String courseTitle; + if (preferences.getBoolean(MinervaFragment.PREF_PREFIX_EVENT_ACRONYM, true)) { + courseTitle = StringUtils.generateAcronymFor(item.getCourse().getTitle()); + } else { + courseTitle = item.getCourse().getTitle(); + } + title = context.getString(R.string.minerva_calendar_device_event_title, courseTitle, item.getTitle()); + } else { + title = item.getTitle(); + } + return title; + } + + /** + * Get the description for an event in the calendar, intended for the device calendar. This method will append the + * course's name to the description, if the user preference for prefixing event titles was turned on. + * + * @param item The item to get the description from. + * + * @return The description. + */ + private String getDescriptionFor(AgendaItem item) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + if (preferences.getBoolean(MinervaFragment.PREF_PREFIX_EVENT_TITLES, false)) { + String original = item.getContent(); + String description = context.getString(R.string.minerva_calendar_device_description, item.getCourse().getTitle()); + if (TextUtils.isEmpty(original)) { + return description; + } else { + return original + "\n\n" + description; + } + } else { + return item.getContent(); + } + } } diff --git a/app/src/main/java/be/ugent/zeus/hydra/ui/preferences/MinervaFragment.java b/app/src/main/java/be/ugent/zeus/hydra/ui/preferences/MinervaFragment.java index b09da5cd3..23471a354 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/ui/preferences/MinervaFragment.java +++ b/app/src/main/java/be/ugent/zeus/hydra/ui/preferences/MinervaFragment.java @@ -28,9 +28,11 @@ public class MinervaFragment extends PreferenceFragment { public static final String PREF_ANNOUNCEMENT_NOTIFICATION_EMAIL = "pref_minerva_announcement_notification_email"; public static final String PREF_USE_MOBILE_URL = "pref_minerva_use_mobile_url"; - public static final String PREF_DETECT_DUPLICATES = "pref_minerva_detect_duplicates"; + public static final String PREF_PREFIX_EVENT_TITLES = "pref_minerva_prefix_event_titles"; + public static final String PREF_PREFIX_EVENT_ACRONYM = "pref_minerva_prefix_event_acronym"; + //In seconds public static final String PREF_DEFAULT_SYNC_FREQUENCY = "86400"; public static final boolean PREF_DEFAULT_ANNOUNCEMENT_NOTIFICATION_EMAIL = false; @@ -41,6 +43,12 @@ public class MinervaFragment extends PreferenceFragment { private boolean oldDetectDuplicates; private boolean newDetectDuplicates; + private boolean oldPrefixEventTitles; + private boolean newPrefixEventTitles; + + private boolean oldPrefixAcronyms; + private boolean newPrefixAcronyms; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -63,9 +71,17 @@ public void onCreate(Bundle savedInstanceState) { oldDetectDuplicates = preferences.getBoolean(PREF_DETECT_DUPLICATES, false); newDetectDuplicates = oldDetectDuplicates; + oldPrefixEventTitles = preferences.getBoolean(PREF_PREFIX_EVENT_TITLES, false); + newPrefixEventTitles = oldPrefixEventTitles; + + oldPrefixAcronyms = preferences.getBoolean(PREF_PREFIX_EVENT_ACRONYM, true); + newPrefixAcronyms = oldPrefixAcronyms; Preference intervalPreference = findPreference(PREF_SYNC_FREQUENCY); Preference detectPreference = findPreference(PREF_DETECT_DUPLICATES); + Preference prefixTitles = findPreference(PREF_PREFIX_EVENT_TITLES); + Preference prefixAbbreviations = findPreference(PREF_PREFIX_EVENT_ACRONYM); + prefixAbbreviations.setEnabled(oldPrefixEventTitles); intervalPreference.setOnPreferenceChangeListener((preference, newValue) -> { newInterval = Integer.parseInt((String) newValue); @@ -77,6 +93,17 @@ public void onCreate(Bundle savedInstanceState) { return true; }); + prefixTitles.setOnPreferenceChangeListener((preference, newValue) -> { + newPrefixEventTitles = (boolean) newValue; + prefixAbbreviations.setEnabled(newPrefixEventTitles); + return true; + }); + + prefixAbbreviations.setOnPreferenceChangeListener((preference, newValue) -> { + newPrefixAcronyms = (boolean) newValue; + return true; + }); + if(!AccountUtils.hasAccount(getAppContext())) { intervalPreference.setEnabled(false); } @@ -89,7 +116,7 @@ public void onPause() { if (hasAccount && oldInterval != newInterval) { SyncUtils.changeSyncFrequency(getAppContext(), MinervaConfig.SYNC_AUTHORITY, newInterval); } - if (hasAccount && oldDetectDuplicates != newDetectDuplicates) { + if (hasAccount && (oldDetectDuplicates != newDetectDuplicates || oldPrefixEventTitles != newPrefixEventTitles || oldPrefixAcronyms != newPrefixAcronyms)) { Bundle bundle = new Bundle(); bundle.putBoolean(MinervaAdapter.SYNC_ANNOUNCEMENTS, false); SyncUtils.requestSync(AccountUtils.getAccount(getAppContext()), MinervaConfig.SYNC_AUTHORITY, bundle); diff --git a/app/src/main/java/be/ugent/zeus/hydra/utils/StringUtils.java b/app/src/main/java/be/ugent/zeus/hydra/utils/StringUtils.java index 99dfd3a2b..111cf47b7 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/utils/StringUtils.java +++ b/app/src/main/java/be/ugent/zeus/hydra/utils/StringUtils.java @@ -31,4 +31,34 @@ public static String convertStreamToString(@NonNull InputStream is) { java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } + + /** + * Generate an acronym for a string. This will split the string on whitespace, take the first letter of every word, + * capitalize that first letter and concat the result. If a word does not contain any letters, it is fully added. + * + * For example, {@code Algoritmen en datastructuren III} will become {@code AEDIII}. + * + * TODO: can we make this less dumb, and ignore words as 'en', 'of', etc? + * + * @param name The string to acronymize. + * + * @return The acronym. + */ + public static String generateAcronymFor(String name) { + StringBuilder result = new StringBuilder(); + for (String word : name.split("\\s")) { + int i = 0; + char c; + do { + c = word.charAt(i++); + } while (!Character.isLetter(c) && i < word.length()); + if (Character.isLetter(c)) { + result.append(Character.toUpperCase(c)); + } else { + // There are no letters in this part, so just add it completely. + result.append(word); + } + } + return result.toString(); + } } \ 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 20bb9d711..3603b61ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - + Hydra @@ -204,6 +204,12 @@ Zonder locatie Licenties %1$s • %2$s + + %1$s: %2$s + + + Les van het vak %1$s + %d aankondiging %d aankondigingen diff --git a/app/src/main/res/xml/pref_minerva.xml b/app/src/main/res/xml/pref_minerva.xml index 624e5e4ee..994fa45a2 100644 --- a/app/src/main/res/xml/pref_minerva.xml +++ b/app/src/main/res/xml/pref_minerva.xml @@ -44,6 +44,19 @@ + + + + + Date: Tue, 31 Oct 2017 22:57:31 +0100 Subject: [PATCH 2/2] Fix spelling --- app/src/main/res/xml/pref_minerva.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/xml/pref_minerva.xml b/app/src/main/res/xml/pref_minerva.xml index 994fa45a2..3934fccc2 100644 --- a/app/src/main/res/xml/pref_minerva.xml +++ b/app/src/main/res/xml/pref_minerva.xml @@ -53,7 +53,7 @@