Implemented calendar integration

This commit is contained in:
Sebastian Seedorf
2019-10-28 18:56:12 +01:00
parent e5d7d1faae
commit 22b42ff3f4
9 changed files with 249 additions and 7 deletions

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.sebse.fuplanner">
<uses-permission
@@ -21,6 +22,10 @@
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<!-- Calendar integration -->
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
@@ -58,7 +63,6 @@
android:syncable="true">
</provider>
<activity
android:name=".services.fulogin.FUAuthenticatorActivity"
android:label="@string/title_activity_fuauthenticator" />

View File

@@ -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);
}
};
}
}

View File

@@ -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);
}
}

View File

@@ -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<ArrayList<Resource>> {
// 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");
}

View File

@@ -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<Event> differencesAdd = new ArrayList<>();
//ArrayList<Pair<Event, Event>> differencesUpd = new ArrayList<>();
//ArrayList<Event> 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,19 +174,158 @@ 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 <T> void sendNotifications(Iterable<T> oldList, Iterable<T> newList, String title, StringInterface<T> titleInterface, StringInterface<T> idInterface, String moduleId, int modulePart, @StringRes int updateRes, @StringRes int addRes, @StringRes int removeRes) {
sendNotifications(oldList, newList, title, titleInterface, idInterface, moduleId, modulePart, updateRes, addRes, removeRes, null, null, null);
}
private <T> void sendNotifications(Iterable<T> oldList, Iterable<T> newList, String title, StringInterface<T> titleInterface, StringInterface<T> idInterface, String moduleId, int modulePart, @StringRes int updateRes, @StringRes int addRes, @StringRes int removeRes, ArrayList<T> changesAdd, ArrayList<Pair<T, T>> changesUpd, ArrayList<T> changesDel) {
if (oldList == null || newList == null) {
return;
}
@@ -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);
}
}
}

View File

@@ -52,6 +52,8 @@
<string name="pref_sync_frequency_title">Synchronisationshäufigkeit</string>
<string name="pref_sync_frequency_summary">Stellt Häufigkeit der automatischen Synchronisation ein</string>
<string name="pref_sync_frequency_dialog">Sync-Frequenz</string>
<string name="pref_add_calendar_title">Zum Kalender hinzufügen</string>
<string name="pref_add_calendar_summary">Wenn ausgewählt, wird </string>
<string name="meals">Hauptgerichte</string>
<string name="special_meals">Spezial Gerichte</string>
<string name="side_dishes">Beilagen</string>

View File

@@ -77,6 +77,9 @@
<string name="pref_night_mode" translatable="false">pref_night_mode</string>
<string name="pref_night_mode_default" translatable="false">auto</string>
<string name="pref_add_calendar" translatable="false">pref_add_calendar</string>
<string name="pref_add_calendar_default" translatable="false">false</string>
<!-- Other preferences -->
<string name="pref_last_visited_news" translatable="false">pref_last_visited_news</string>

View File

@@ -11,7 +11,7 @@
<string name="schedule">Schedule</string>
<string name="courses">Courses</string>
<string name="canteen_plan">Canteen Plan</string>
<string name="settings">Settings</string>
<string name="settings">Preferences</string>
<string name="options">Options</string>
<string name="log_out">Log out</string>
<string name="share">Share</string>
@@ -59,6 +59,8 @@
<string name="pref_sync_frequency_title">Sync frequency</string>
<string name="pref_sync_frequency_summary">Set automatic background sync frequency</string>
<string name="pref_sync_frequency_dialog">Frequency Selection</string>
<string name="pref_add_calendar_title">Add to Calendar</string>
<string name="pref_add_calendar_summary">If checked schedule will be added to your calendar app</string>
<string name="meals">Meals</string>
<string name="special_meals">Special meals</string>
<string name="side_dishes">Side Dishes</string>

View File

@@ -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" />
<CheckBoxPreference
android:key="@string/pref_add_calendar"
android:defaultValue="@string/pref_add_calendar_default"
android:title="@string/pref_add_calendar_title"
android:summary="@string/pref_add_calendar_summary" />
<PreferenceScreen
android:title="@string/open_data_policy"
android:summary="@string/open_data_policy_summary">