From 22b42ff3f4d568d163bf16ac75a2b181f819961f Mon Sep 17 00:00:00 2001 From: Sebastian Seedorf Date: Mon, 28 Oct 2019 18:56:12 +0100 Subject: [PATCH] Implemented calendar integration --- app/src/main/AndroidManifest.xml | 6 +- .../fuplanner/fragments/PrefsFragment.java | 53 ++++++ .../fuplanner/services/kvv/ModulesList.java | 3 +- .../services/kvv/ModulesResources.java | 4 + .../services/kvv/sync/KVVSyncAdapter.java | 176 +++++++++++++++++- app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values/preferences.xml | 3 + app/src/main/res/values/strings.xml | 4 +- app/src/main/res/xml/preferences.xml | 5 + 9 files changed, 249 insertions(+), 7 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 04fe56a..46af935 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + + + + - diff --git a/app/src/main/java/de/sebse/fuplanner/fragments/PrefsFragment.java b/app/src/main/java/de/sebse/fuplanner/fragments/PrefsFragment.java index c1dfaad..3ad38ca 100644 --- a/app/src/main/java/de/sebse/fuplanner/fragments/PrefsFragment.java +++ b/app/src/main/java/de/sebse/fuplanner/fragments/PrefsFragment.java @@ -1,24 +1,38 @@ package de.sebse.fuplanner.fragments; +import android.Manifest; import android.accounts.Account; import android.content.ContentResolver; +import android.content.Context; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; + +import java.util.Arrays; + import de.sebse.fuplanner.MainActivity; import de.sebse.fuplanner.R; import de.sebse.fuplanner.services.fulogin.AccountGeneral; import de.sebse.fuplanner.services.kvv.sync.KVVContentProvider; +import de.sebse.fuplanner.services.kvv.ui.Download; import de.sebse.fuplanner.tools.CustomAccountManager; +import de.sebse.fuplanner.tools.MainActivityListener; import de.sebse.fuplanner.tools.Preferences; +import de.sebse.fuplanner.tools.RequestPermissionsResultListener; import de.sebse.fuplanner.tools.logging.Logger; public class PrefsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { + private MainActivityListener mMainActivityListener; + public static PrefsFragment newInstance() { PrefsFragment fragment = new PrefsFragment(); Bundle args = new Bundle(); @@ -84,5 +98,44 @@ public class PrefsFragment extends PreferenceFragmentCompat implements SharedPre break; } } + + if (s.equals(requireContext().getString(R.string.pref_add_calendar)) + && getActivity() != null + && Preferences.getBoolean(getActivity(), R.string.pref_add_calendar)) { + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED + || ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.WRITE_CALENDAR, Manifest.permission.READ_CALENDAR}, 1); + } + } + } + + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (context instanceof MainActivityListener) { + mMainActivityListener = (MainActivityListener) context; + mMainActivityListener.onTitleTextChange(R.string.settings); + mMainActivityListener.addRequestPermissionsResultListener(getRequestPermissionsResultListener(), "PrefFragment"); + } else + throw new RuntimeException(context.toString() + " must implement MainActivityListener"); + } + + @Override + public void onDetach() { + super.onDetach(); + mMainActivityListener.removeRequestPermissionsResultListener("PrefFragment"); + mMainActivityListener = null; + } + + private RequestPermissionsResultListener getRequestPermissionsResultListener() { + return (requestCode, permissions, grantResults) -> { + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_CALENDAR) != PackageManager.PERMISSION_GRANTED + || ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) { + Preferences.setBoolean(requireContext(), R.string.pref_add_calendar, false); + setPreferenceScreen(null); + setPreferencesFromResource(R.xml.preferences, null); + } + }; } } \ No newline at end of file diff --git a/app/src/main/java/de/sebse/fuplanner/services/kvv/ModulesList.java b/app/src/main/java/de/sebse/fuplanner/services/kvv/ModulesList.java index 2d6cf9b..87ad5ea 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/kvv/ModulesList.java +++ b/app/src/main/java/de/sebse/fuplanner/services/kvv/ModulesList.java @@ -104,9 +104,10 @@ public class ModulesList extends HTTPService { } catch (FileNotFoundException ignored) { } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); + delete(); } if (this.mModules == null) { - recv(success -> {}, log::e); + recv(success -> {}, log::e, true); } } diff --git a/app/src/main/java/de/sebse/fuplanner/services/kvv/ModulesResources.java b/app/src/main/java/de/sebse/fuplanner/services/kvv/ModulesResources.java index 63451f4..7dab338 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/kvv/ModulesResources.java +++ b/app/src/main/java/de/sebse/fuplanner/services/kvv/ModulesResources.java @@ -3,6 +3,8 @@ package de.sebse.fuplanner.services.kvv; import android.content.Context; import android.os.Environment; +import androidx.core.content.ContextCompat; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -369,6 +371,8 @@ public class ModulesResources extends PartModules> { // Saves file in folder: DOWNLOADS/moduleName File folder = new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS), moduleName); + //ContextCompat.checkSelfPermission(getContext(), ) + log.d("FILE", folder.toString()); if (!folder.mkdirs()) { log.w( "Directory not created"); } diff --git a/app/src/main/java/de/sebse/fuplanner/services/kvv/sync/KVVSyncAdapter.java b/app/src/main/java/de/sebse/fuplanner/services/kvv/sync/KVVSyncAdapter.java index b0da80e..edab6bc 100644 --- a/app/src/main/java/de/sebse/fuplanner/services/kvv/sync/KVVSyncAdapter.java +++ b/app/src/main/java/de/sebse/fuplanner/services/kvv/sync/KVVSyncAdapter.java @@ -1,26 +1,40 @@ package de.sebse.fuplanner.services.kvv.sync; +import android.Manifest; import android.accounts.Account; +import android.accounts.AccountManager; import android.content.AbstractThreadedSyncAdapter; import android.content.ComponentName; import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SyncResult; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; import android.os.IBinder; +import android.provider.CalendarContract.Calendars; +import android.provider.CalendarContract.Events; +import android.util.Pair; import java.util.ArrayList; +import java.util.Calendar; import java.util.Iterator; import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; + import de.sebse.fuplanner.R; import de.sebse.fuplanner.fragments.moddetails.ModulePart; import de.sebse.fuplanner.services.kvv.KVV; import de.sebse.fuplanner.services.kvv.types.Announcement; import de.sebse.fuplanner.services.kvv.types.Assignment; import de.sebse.fuplanner.services.kvv.types.AssignmentList; +import de.sebse.fuplanner.services.kvv.types.Event; import de.sebse.fuplanner.services.kvv.types.EventList; import de.sebse.fuplanner.services.kvv.types.Grade; import de.sebse.fuplanner.services.kvv.types.Gradebook; @@ -28,9 +42,11 @@ import de.sebse.fuplanner.services.kvv.types.Modules; import de.sebse.fuplanner.services.kvv.types.Resource; import de.sebse.fuplanner.tools.CustomNotificationManager; import de.sebse.fuplanner.tools.NewAsyncQueue; +import de.sebse.fuplanner.tools.Preferences; import de.sebse.fuplanner.tools.UtilsDate; import de.sebse.fuplanner.tools.logging.Logger; +import static android.provider.CalendarContract.CALLER_IS_SYNCADAPTER; import static de.sebse.fuplanner.MainActivity.FRAGMENT_MODULES_DETAILS; public class KVVSyncAdapter extends AbstractThreadedSyncAdapter { @@ -98,7 +114,7 @@ public class KVVSyncAdapter extends AbstractThreadedSyncAdapter { String authority, ContentProviderClient provider, SyncResult syncResult) { - if (!mBound) { + if (!mBound && !mWaitForBound) { Intent intent = new Intent(getContext(), KVV.class); getContext().bindService(intent, mConnection, Context.BIND_AUTO_CREATE); mWaitForBound = true; @@ -138,9 +154,13 @@ public class KVVSyncAdapter extends AbstractThreadedSyncAdapter { sendNotifications(assignments, module.assignments, module.title, Assignment::getTitle, Assignment::getId, module.getID(), ModulePart.ASSIGNMENT, R.string.assignment_updated, R.string.assignment_added, R.string.assignment_removed); + //ArrayList differencesAdd = new ArrayList<>(); + //ArrayList> differencesUpd = new ArrayList<>(); + //ArrayList differencesDel = new ArrayList<>(); sendNotifications(events, module.events, module.title, evt -> evt.getTitle()+" - "+UtilsDate.getModifiedDate(evt.getStartDate()), event -> event.getStartDate() +event.getType()+event.getTitle(), module.getID(), ModulePart.EVENT, - R.string.event_updated, R.string.event_added, R.string.event_removed); + R.string.event_updated, R.string.event_added, R.string.event_removed/*, + differencesAdd, differencesUpd, differencesDel*/); sendNotifications(gradebook, module.gradebook, module.title, Grade::getItemName, Grade::getItemName, module.getID(), ModulePart.GRADEBOOK, R.string.gradebook_updated, R.string.gradebook_added, R.string.gradebook_removed); @@ -154,20 +174,159 @@ public class KVVSyncAdapter extends AbstractThreadedSyncAdapter { if (--latch[0] == 0) mQueue.next(); }, true); } + + // Add events to calendar + if (createCalendar(account)) { + iterator = success.latestSemesterIterator(); + while (iterator.hasNext()) { + Modules.Module module = iterator.next(); + addToCalendar(module.events, account); + } + forceSync(); + } }, msg -> { log.e(msg); mQueue.next(); }, true); }); mQueue.add(() -> { + if (mBound) { + getContext().unbindService(mConnection); + } mBound = false; mKVV = null; - getContext().unbindService(mConnection); + mQueue.next(); }); } + static Uri asSyncAdapter(Uri uri, String account, String accountType) { + return uri.buildUpon() + .appendQueryParameter(CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(Calendars.ACCOUNT_NAME, account) + .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build(); + } + + private static Uri createCalendarWithName(Context ctx, Account account, String calendarName) { + String accountName = account.name; + String accountType = account.type; + Uri target = asSyncAdapter(Calendars.CONTENT_URI, accountName, accountType); + + ContentValues values = new ContentValues(); + values.put(Calendars._ID, calendarName.hashCode()); + values.put(Calendars.ACCOUNT_NAME, accountName); + values.put(Calendars.ACCOUNT_TYPE, accountType); + values.put(Calendars.NAME, calendarName); + values.put(Calendars.CALENDAR_DISPLAY_NAME, calendarName); + values.put(Calendars.CALENDAR_COLOR, 0x00FF00); + values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ); + values.put(Calendars.OWNER_ACCOUNT, accountName); + values.put(Calendars.VISIBLE, 1); + values.put(Calendars.SYNC_EVENTS, 1); + values.put(Calendars.CALENDAR_TIME_ZONE, "Europe/Berlin"); + values.put(Calendars.CAN_PARTIALLY_UPDATE, 1); + values.put(Calendars.CAL_SYNC1, System.currentTimeMillis()); + + return ctx.getContentResolver().insert(target, values); + } + + private int createEvents(Context ctx, Account account, String calendarName, EventList events) { + String accountName = account.name; + String accountType = account.type; + Uri target = asSyncAdapter(Events.CONTENT_URI, accountName, accountType); + + ContentValues[] contentValues = new ContentValues[events.size()]; + for (int i = 0; i < contentValues.length; i++) { + Event event = events.get(i); + ContentValues values = new ContentValues(); + //values.put(Events._ID, event.hashCode()); + values.put(Events.TITLE, event.getTitle()); + values.put(Events.DTSTART, event.getStartDate()); + values.put(Events.DTEND, event.getEndDate()); + values.put(Events.CALENDAR_ID, calendarName.hashCode()); + contentValues[i] = values; + //ctx.getContentResolver().insert(target, values); + } + log.d(contentValues.length); + return ctx.getContentResolver().bulkInsert(target, contentValues); + } + + private static Cursor getCalendars(Context ctx, Account account) { + Uri target = asSyncAdapter(Calendars.CONTENT_URI, account.name, account.type); + + return ctx.getContentResolver().query(target, new String[]{Calendars.CALENDAR_DISPLAY_NAME, Calendars.ACCOUNT_NAME}, null, null, null); + } + + private static Cursor getEvents(Context ctx, Account account) { + Uri target = asSyncAdapter(Events.CONTENT_URI, account.name, account.type); + + return ctx.getContentResolver().query(target, new String[]{Events.TITLE, Events.DTSTART}, null, null, null); + } + + private static int deleteCalendars(Context ctx, Account account) { + String accountName = account.name; + String accountType = account.type; + Uri target = asSyncAdapter(Calendars.CONTENT_URI, account.name, account.type); + + ContentValues values = new ContentValues(); + values.put(Calendars.ACCOUNT_NAME, accountName); + values.put(Calendars.ACCOUNT_TYPE, accountType); + + return ctx.getContentResolver().delete(target, null, null); + } + + private void addToCalendar(EventList events, Account account) { + if (events == null) { + return; + } + if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED) { + boolean integrationEnabled = Preferences.getBoolean(getContext(), R.string.pref_add_calendar); + if (integrationEnabled) { + createEvents(getContext(), account, getContext().getString(R.string.app_name), events); + } + } + } + + private boolean createCalendar(Account account) { + if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED) { + boolean integrationEnabled = Preferences.getBoolean(getContext(), R.string.pref_add_calendar); + if (integrationEnabled) { + //log.w("No calendar found! Add calendar..."); + deleteCalendars(getContext(), account); + createCalendarWithName(getContext(), account, getContext().getString(R.string.app_name)); + return true; + } else { + log.w("Calendar found and integration disabled! Delete calendar..."); + deleteCalendars(getContext(), account); + return false; + } + } else { + log.w("Permission calendar not granted!"); + return false; + } + } + + private void forceSync() { + // Force a sync + Bundle extras = new Bundle(); + extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); + extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); + AccountManager am = AccountManager.get(getContext()); + Account[] acc = am.getAccountsByType("com.google"); + Account account = null; + if (acc.length>0) { + account = acc[0]; + ContentResolver.requestSync(account, "com.android.calendar", extras); + } + } + private void sendNotifications(Iterable oldList, Iterable newList, String title, StringInterface titleInterface, StringInterface idInterface, String moduleId, int modulePart, @StringRes int updateRes, @StringRes int addRes, @StringRes int removeRes) { - if (oldList == null || newList == null) { + sendNotifications(oldList, newList, title, titleInterface, idInterface, moduleId, modulePart, updateRes, addRes, removeRes, null, null, null); + } + + private void sendNotifications(Iterable oldList, Iterable newList, String title, StringInterface titleInterface, StringInterface idInterface, String moduleId, int modulePart, @StringRes int updateRes, @StringRes int addRes, @StringRes int removeRes, ArrayList changesAdd, ArrayList> changesUpd, ArrayList changesDel) { + if (oldList == null || newList == null) { return; } ArrayList obsoletes = new ArrayList<>(); @@ -182,6 +341,9 @@ public class KVVSyncAdapter extends AbstractThreadedSyncAdapter { found = true; if (newEntry.hashCode() != oldEntry.hashCode()) { CustomNotificationManager.sendNotification(getContext(), getContext().getString(updateRes, title), titleInterface.get(newEntry), FRAGMENT_MODULES_DETAILS, targetData); + if (changesUpd != null) { + changesUpd.add(new Pair<>(oldEntry, newEntry)); + } } obsoletes.remove(oldEntry); break; @@ -189,10 +351,16 @@ public class KVVSyncAdapter extends AbstractThreadedSyncAdapter { } if (!found) { CustomNotificationManager.sendNotification(getContext(), getContext().getString(addRes, title), titleInterface.get(newEntry), FRAGMENT_MODULES_DETAILS, targetData); + if (changesAdd != null) { + changesAdd.add(newEntry); + } } } for (T oldEntry: obsoletes) { CustomNotificationManager.sendNotification(getContext(), getContext().getString(removeRes, title), titleInterface.get(oldEntry), FRAGMENT_MODULES_DETAILS, targetData); + if (changesDel != null) { + changesDel.add(oldEntry); + } } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index dfa61fa..ab5b86f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -52,6 +52,8 @@ Synchronisationshäufigkeit Stellt Häufigkeit der automatischen Synchronisation ein Sync-Frequenz + Zum Kalender hinzufügen + Wenn ausgewählt, wird Hauptgerichte Spezial Gerichte Beilagen diff --git a/app/src/main/res/values/preferences.xml b/app/src/main/res/values/preferences.xml index 716100b..0f2a829 100644 --- a/app/src/main/res/values/preferences.xml +++ b/app/src/main/res/values/preferences.xml @@ -77,6 +77,9 @@ pref_night_mode auto + pref_add_calendar + false + pref_last_visited_news diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f0b5456..6d16631 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,7 +11,7 @@ Schedule Courses Canteen Plan - Settings + Preferences Options Log out Share @@ -59,6 +59,8 @@ Sync frequency Set automatic background sync frequency Frequency Selection + Add to Calendar + If checked schedule will be added to your calendar app Meals Special meals Side Dishes diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 5dceeb9..6a97660 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -33,6 +33,11 @@ android:entries="@array/pref_sync_frequency_entries" android:entryValues="@array/pref_sync_frequency_values" android:dialogTitle="@string/pref_sync_frequency_dialog" /> +