mirror of
https://github.com/MatomoCamp/matomocamp-companion-android.git
synced 2024-09-19 16:13:46 +02:00
Added bookmarks export to iCalendar file, closes #5
This commit is contained in:
parent
9e4efd69e2
commit
01e1c25f36
8 changed files with 387 additions and 10 deletions
|
@ -117,6 +117,11 @@
|
|||
android:name=".providers.SearchSuggestionProvider"
|
||||
android:authorities="${applicationId}.search"
|
||||
android:exported="true"/>
|
||||
<provider
|
||||
android:name=".providers.BookmarksExportProvider"
|
||||
android:authorities="${applicationId}.bookmarks"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -1,7 +1,9 @@
|
|||
package be.digitalia.fosdem.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
|
@ -20,6 +22,7 @@ import be.digitalia.fosdem.R;
|
|||
import be.digitalia.fosdem.adapters.BookmarksAdapter;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
||||
import be.digitalia.fosdem.providers.BookmarksExportProvider;
|
||||
|
||||
/**
|
||||
* Bookmarks list, optionally filterable.
|
||||
|
@ -85,10 +88,13 @@ public class BookmarksListFragment extends RecyclerViewFragment implements Loade
|
|||
inflater.inflate(R.menu.bookmarks, menu);
|
||||
filterMenuItem = menu.findItem(R.id.filter);
|
||||
upcomingOnlyMenuItem = menu.findItem(R.id.upcoming_only);
|
||||
updateOptionsMenu();
|
||||
updateFilterMenuItem();
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
|
||||
menu.findItem(R.id.export_bookmarks).setEnabled(false).setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateOptionsMenu() {
|
||||
private void updateFilterMenuItem() {
|
||||
if (filterMenuItem != null) {
|
||||
filterMenuItem.setIcon(upcomingOnly ?
|
||||
R.drawable.ic_filter_list_selected_white_24dp
|
||||
|
@ -109,12 +115,16 @@ public class BookmarksListFragment extends RecyclerViewFragment implements Loade
|
|||
switch (item.getItemId()) {
|
||||
case R.id.upcoming_only:
|
||||
upcomingOnly = !upcomingOnly;
|
||||
updateOptionsMenu();
|
||||
updateFilterMenuItem();
|
||||
SharedPreferencesCompat.EditorCompat.getInstance().apply(
|
||||
getActivity().getPreferences(Context.MODE_PRIVATE).edit().putBoolean(PREF_UPCOMING_ONLY, upcomingOnly)
|
||||
);
|
||||
getLoaderManager().restartLoader(BOOKMARKS_LOADER_ID, null, this);
|
||||
return true;
|
||||
case R.id.export_bookmarks:
|
||||
Intent exportIntent = BookmarksExportProvider.getIntent(getActivity());
|
||||
startActivity(Intent.createChooser(exportIntent, getString(R.string.export_bookmarks)));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,234 @@
|
|||
package be.digitalia.fosdem.providers;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.ShareCompat;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import be.digitalia.fosdem.BuildConfig;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.api.FosdemUrls;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.utils.ICalendarWriter;
|
||||
import be.digitalia.fosdem.utils.StringUtils;
|
||||
|
||||
/**
|
||||
* Content Provider generating the current bookmarks list in iCalendar format.
|
||||
*/
|
||||
public class BookmarksExportProvider extends ContentProvider {
|
||||
|
||||
private static final Uri URI = new Uri.Builder()
|
||||
.scheme("content")
|
||||
.authority(BuildConfig.APPLICATION_ID + ".bookmarks")
|
||||
.appendPath("bookmarks.ics")
|
||||
.build();
|
||||
private static final String TYPE = "text/calendar";
|
||||
|
||||
private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
|
||||
|
||||
|
||||
public static Intent getIntent(Activity activity) {
|
||||
final Intent exportIntent;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
// Supports granting read permission for the attached shared file
|
||||
exportIntent = ShareCompat.IntentBuilder.from(activity)
|
||||
.setStream(URI)
|
||||
.setType(TYPE)
|
||||
.getIntent();
|
||||
} else {
|
||||
// Fallback: open file directly
|
||||
exportIntent = new Intent(Intent.ACTION_VIEW).setDataAndType(URI, TYPE);
|
||||
}
|
||||
exportIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
return exportIntent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, ContentValues values) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
if (projection == null) {
|
||||
projection = COLUMNS;
|
||||
}
|
||||
String[] cols = new String[projection.length];
|
||||
Object[] values = new Object[projection.length];
|
||||
int i = 0;
|
||||
for (String col : projection) {
|
||||
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
|
||||
cols[i] = OpenableColumns.DISPLAY_NAME;
|
||||
values[i++] = getContext().getString(R.string.export_bookmarks_file_name, DatabaseManager.getInstance().getYear());
|
||||
} else if (OpenableColumns.SIZE.equals(col)) {
|
||||
cols[i] = OpenableColumns.SIZE;
|
||||
// Unknown size, content will be generated on-the-fly
|
||||
values[i++] = 1024L;
|
||||
}
|
||||
}
|
||||
|
||||
cols = copyOf(cols, i);
|
||||
values = copyOf(values, i);
|
||||
|
||||
final MatrixCursor cursor = new MatrixCursor(cols, 1);
|
||||
cursor.addRow(values);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
|
||||
@Nullable
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
|
||||
throw new FileNotFoundException("Bookmarks export is not supported for this Android version");
|
||||
}
|
||||
|
||||
try {
|
||||
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
|
||||
new DownloadThread(new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])).start();
|
||||
return pipe[0];
|
||||
} catch (IOException e) {
|
||||
throw new FileNotFoundException("Could not open pipe");
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] copyOf(String[] original, int newLength) {
|
||||
final String[] result = new String[newLength];
|
||||
System.arraycopy(original, 0, result, 0, newLength);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Object[] copyOf(Object[] original, int newLength) {
|
||||
final Object[] result = new Object[newLength];
|
||||
System.arraycopy(original, 0, result, 0, newLength);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
static class DownloadThread extends Thread {
|
||||
private final ICalendarWriter writer;
|
||||
|
||||
private final DateFormat dateFormat;
|
||||
private final String dtStamp;
|
||||
private final TextUtils.StringSplitter personsSplitter = new StringUtils.SimpleStringSplitter(", ");
|
||||
|
||||
DownloadThread(OutputStream out) {
|
||||
this.writer = new ICalendarWriter(new BufferedWriter(new OutputStreamWriter(out)));
|
||||
|
||||
// Format all times in GMT
|
||||
this.dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US);
|
||||
this.dateFormat.setTimeZone(TimeZone.getTimeZone("GMT+0"));
|
||||
this.dtStamp = dateFormat.format(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
final Cursor cursor = DatabaseManager.getInstance().getBookmarks(0L);
|
||||
try {
|
||||
writer.write("BEGIN", "VCALENDAR");
|
||||
writer.write("VERSION", "2.0");
|
||||
writer.write("PRODID", "-//" + BuildConfig.APPLICATION_ID + "//NONSGML " + BuildConfig.VERSION_NAME + "//EN");
|
||||
|
||||
Event event = null;
|
||||
while (cursor.moveToNext()) {
|
||||
event = DatabaseManager.toEvent(cursor, event);
|
||||
writeEvent(event);
|
||||
}
|
||||
|
||||
writer.write("END", "VCALENDAR");
|
||||
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
} finally {
|
||||
try {
|
||||
writer.close();
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeEvent(Event event) throws IOException {
|
||||
writer.write("BEGIN", "VEVENT");
|
||||
|
||||
final int year = DatabaseManager.getInstance().getYear();
|
||||
writer.write("UID", String.format(Locale.US, "%1$d@%2$d@%3$s", event.getId(), year, BuildConfig.APPLICATION_ID));
|
||||
writer.write("DTSTAMP", dtStamp);
|
||||
if (event.getStartTime() != null) {
|
||||
writer.write("DTSTART", dateFormat.format(event.getStartTime()));
|
||||
}
|
||||
if (event.getEndTime() != null) {
|
||||
writer.write("DTEND", dateFormat.format(event.getEndTime()));
|
||||
}
|
||||
writer.write("SUMMARY", event.getTitle());
|
||||
String description = event.getAbstractText();
|
||||
if (TextUtils.isEmpty(description)) {
|
||||
description = event.getDescription();
|
||||
}
|
||||
if (!TextUtils.isEmpty(description)) {
|
||||
writer.write("DESCRIPTION", StringUtils.stripHtml(description));
|
||||
writer.write("X-ALT-DESC", description);
|
||||
}
|
||||
writer.write("CLASS", "PUBLIC");
|
||||
writer.write("CATEGORIES", event.getTrack().getName());
|
||||
writer.write("URL", event.getUrl());
|
||||
writer.write("LOCATION", event.getRoomName());
|
||||
|
||||
personsSplitter.setString(event.getPersonsSummary());
|
||||
for (String name : personsSplitter) {
|
||||
String key = String.format(Locale.US, "ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUAL;CN=\"%1$s\"", name);
|
||||
String url = FosdemUrls.getPerson(StringUtils.toSlug(name), year);
|
||||
writer.write(key, url);
|
||||
}
|
||||
|
||||
writer.write("END", "VEVENT");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package be.digitalia.fosdem.utils;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
|
||||
/**
|
||||
* Simple wrapper to write to iCalendar file format.
|
||||
*/
|
||||
public class ICalendarWriter implements Closeable {
|
||||
|
||||
private static final String CRLF = "\r\n";
|
||||
|
||||
private final Writer writer;
|
||||
|
||||
public ICalendarWriter(@NonNull Writer writer) {
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
public void write(@NonNull String key, @Nullable String value) throws IOException {
|
||||
if (value != null) {
|
||||
writer.write(key);
|
||||
writer.write(':');
|
||||
|
||||
// Escape line break sequences
|
||||
final int length = value.length();
|
||||
int start = 0;
|
||||
int end = 0;
|
||||
while (end < length) {
|
||||
final char c = value.charAt(end);
|
||||
if (c == '\r' || c == '\n') {
|
||||
writer.write(value, start, end - start);
|
||||
writer.write(CRLF);
|
||||
writer.write(' ');
|
||||
do {
|
||||
end++;
|
||||
}
|
||||
while ((end < length) && (value.charAt(end) == '\r' || value.charAt(end) == '\n'));
|
||||
start = end;
|
||||
} else {
|
||||
end++;
|
||||
}
|
||||
}
|
||||
writer.write(value, start, end - start);
|
||||
|
||||
writer.write(CRLF);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
writer.close();
|
||||
}
|
||||
}
|
|
@ -1,15 +1,18 @@
|
|||
package be.digitalia.fosdem.utils;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.util.CircularIntArray;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.BulletSpan;
|
||||
import android.text.style.LeadingMarginSpan;
|
||||
|
||||
import org.xml.sax.XMLReader;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
|
@ -33,7 +36,7 @@ public class StringUtils {
|
|||
* @param source string to convert
|
||||
* @return corresponding string without diacritics
|
||||
*/
|
||||
public static String removeDiacritics(String source) {
|
||||
public static String removeDiacritics(@NonNull String source) {
|
||||
final int length = source.length();
|
||||
char[] result = new char[length];
|
||||
char c;
|
||||
|
@ -91,21 +94,21 @@ public class StringUtils {
|
|||
/**
|
||||
* Transforms a name to a slug identifier to be used in a FOSDEM URL.
|
||||
*/
|
||||
public static String toSlug(String source) {
|
||||
public static String toSlug(@NonNull String source) {
|
||||
return replaceNonAlphaGroups(trimNonAlpha(removeDiacritics(source)), '_').toLowerCase(Locale.US);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public static String stripHtml(String html) {
|
||||
public static String stripHtml(@NonNull String html) {
|
||||
return trimEnd(Html.fromHtml(html)).toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public static CharSequence parseHtml(String html, Resources res) {
|
||||
public static CharSequence parseHtml(@NonNull String html, Resources res) {
|
||||
return trimEnd(Html.fromHtml(html, null, new ListsTagHandler(res)));
|
||||
}
|
||||
|
||||
public static CharSequence trimEnd(CharSequence source) {
|
||||
public static CharSequence trimEnd(@NonNull CharSequence source) {
|
||||
int pos = source.length() - 1;
|
||||
while ((pos >= 0) && Character.isWhitespace(source.charAt(pos))) {
|
||||
pos--;
|
||||
|
@ -118,7 +121,7 @@ public class StringUtils {
|
|||
* Converts a room name to a local drawable resource name, by stripping non-alpha chars and converting to lower case. Any letter following a digit will be
|
||||
* ignored, along with the rest of the string.
|
||||
*/
|
||||
public static String roomNameToResourceName(String roomName) {
|
||||
public static String roomNameToResourceName(@NonNull String roomName) {
|
||||
StringBuilder builder = new StringBuilder(ROOM_DRAWABLE_PREFIX.length() + roomName.length());
|
||||
builder.append(ROOM_DRAWABLE_PREFIX);
|
||||
int size = roomName.length();
|
||||
|
@ -200,4 +203,56 @@ public class StringUtils {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A version of Android's SimpleStringSplitter using a String as delimiter.
|
||||
*/
|
||||
public static class SimpleStringSplitter implements TextUtils.StringSplitter, Iterator<String> {
|
||||
private final String mDelimiter;
|
||||
private String mString;
|
||||
private int mPosition;
|
||||
private int mLength;
|
||||
|
||||
/**
|
||||
* Initializes the splitter. setString may be called later.
|
||||
*
|
||||
* @param delimiter the delimiter on which to split
|
||||
*/
|
||||
public SimpleStringSplitter(String delimiter) {
|
||||
mDelimiter = delimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the string to split
|
||||
*
|
||||
* @param string the string to split
|
||||
*/
|
||||
public void setString(String string) {
|
||||
mString = string;
|
||||
mPosition = 0;
|
||||
mLength = mString.length();
|
||||
}
|
||||
|
||||
public Iterator<String> iterator() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasNext() {
|
||||
return mPosition < mLength;
|
||||
}
|
||||
|
||||
public String next() {
|
||||
int end = mString.indexOf(mDelimiter, mPosition);
|
||||
if (end == -1) {
|
||||
end = mLength;
|
||||
}
|
||||
String nextString = mString.substring(mPosition, end);
|
||||
mPosition = end + mDelimiter.length(); // Skip the delimiter.
|
||||
return nextString;
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
9
app/src/main/res/drawable/ic_file_upload_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_file_upload_white_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z"/>
|
||||
</vector>
|
|
@ -13,5 +13,10 @@
|
|||
android:title="@string/upcoming_only"/>
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
android:id="@+id/export_bookmarks"
|
||||
android:icon="@drawable/ic_file_upload_white_24dp"
|
||||
android:title="@string/export_bookmarks"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
</menu>
|
|
@ -28,6 +28,8 @@
|
|||
<!-- Bookmarks -->
|
||||
<string name="filter">Filter</string>
|
||||
<string name="upcoming_only">Upcoming only</string>
|
||||
<string name="export_bookmarks">Export bookmarks</string>
|
||||
<string name="export_bookmarks_file_name">FOSDEM %1$d bookmarks.ics</string>
|
||||
<string name="no_bookmark">No bookmark.</string>
|
||||
<string name="remove_bookmarks">Remove bookmarks</string>
|
||||
<string name="bookmark_conflict_content_description">%1$s\n Other bookmarks are scheduled at the same time.</string>
|
||||
|
|
Loading…
Reference in a new issue