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:name=".providers.SearchSuggestionProvider"
|
||||||
android:authorities="${applicationId}.search"
|
android:authorities="${applicationId}.search"
|
||||||
android:exported="true"/>
|
android:exported="true"/>
|
||||||
|
<provider
|
||||||
|
android:name=".providers.BookmarksExportProvider"
|
||||||
|
android:authorities="${applicationId}.bookmarks"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true"/>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
|
@ -1,7 +1,9 @@
|
||||||
package be.digitalia.fosdem.fragments;
|
package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
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.adapters.BookmarksAdapter;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.db.DatabaseManager;
|
||||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
||||||
|
import be.digitalia.fosdem.providers.BookmarksExportProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bookmarks list, optionally filterable.
|
* Bookmarks list, optionally filterable.
|
||||||
|
@ -85,10 +88,13 @@ public class BookmarksListFragment extends RecyclerViewFragment implements Loade
|
||||||
inflater.inflate(R.menu.bookmarks, menu);
|
inflater.inflate(R.menu.bookmarks, menu);
|
||||||
filterMenuItem = menu.findItem(R.id.filter);
|
filterMenuItem = menu.findItem(R.id.filter);
|
||||||
upcomingOnlyMenuItem = menu.findItem(R.id.upcoming_only);
|
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) {
|
if (filterMenuItem != null) {
|
||||||
filterMenuItem.setIcon(upcomingOnly ?
|
filterMenuItem.setIcon(upcomingOnly ?
|
||||||
R.drawable.ic_filter_list_selected_white_24dp
|
R.drawable.ic_filter_list_selected_white_24dp
|
||||||
|
@ -109,12 +115,16 @@ public class BookmarksListFragment extends RecyclerViewFragment implements Loade
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.upcoming_only:
|
case R.id.upcoming_only:
|
||||||
upcomingOnly = !upcomingOnly;
|
upcomingOnly = !upcomingOnly;
|
||||||
updateOptionsMenu();
|
updateFilterMenuItem();
|
||||||
SharedPreferencesCompat.EditorCompat.getInstance().apply(
|
SharedPreferencesCompat.EditorCompat.getInstance().apply(
|
||||||
getActivity().getPreferences(Context.MODE_PRIVATE).edit().putBoolean(PREF_UPCOMING_ONLY, upcomingOnly)
|
getActivity().getPreferences(Context.MODE_PRIVATE).edit().putBoolean(PREF_UPCOMING_ONLY, upcomingOnly)
|
||||||
);
|
);
|
||||||
getLoaderManager().restartLoader(BOOKMARKS_LOADER_ID, null, this);
|
getLoaderManager().restartLoader(BOOKMARKS_LOADER_ID, null, this);
|
||||||
return true;
|
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;
|
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;
|
package be.digitalia.fosdem.utils;
|
||||||
|
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.util.CircularIntArray;
|
import android.support.v4.util.CircularIntArray;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.text.style.BulletSpan;
|
import android.text.style.BulletSpan;
|
||||||
import android.text.style.LeadingMarginSpan;
|
import android.text.style.LeadingMarginSpan;
|
||||||
|
|
||||||
import org.xml.sax.XMLReader;
|
import org.xml.sax.XMLReader;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,7 +36,7 @@ public class StringUtils {
|
||||||
* @param source string to convert
|
* @param source string to convert
|
||||||
* @return corresponding string without diacritics
|
* @return corresponding string without diacritics
|
||||||
*/
|
*/
|
||||||
public static String removeDiacritics(String source) {
|
public static String removeDiacritics(@NonNull String source) {
|
||||||
final int length = source.length();
|
final int length = source.length();
|
||||||
char[] result = new char[length];
|
char[] result = new char[length];
|
||||||
char c;
|
char c;
|
||||||
|
@ -91,21 +94,21 @@ public class StringUtils {
|
||||||
/**
|
/**
|
||||||
* Transforms a name to a slug identifier to be used in a FOSDEM URL.
|
* 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);
|
return replaceNonAlphaGroups(trimNonAlpha(removeDiacritics(source)), '_').toLowerCase(Locale.US);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public static String stripHtml(String html) {
|
public static String stripHtml(@NonNull String html) {
|
||||||
return trimEnd(Html.fromHtml(html)).toString();
|
return trimEnd(Html.fromHtml(html)).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@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)));
|
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;
|
int pos = source.length() - 1;
|
||||||
while ((pos >= 0) && Character.isWhitespace(source.charAt(pos))) {
|
while ((pos >= 0) && Character.isWhitespace(source.charAt(pos))) {
|
||||||
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
|
* 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.
|
* 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());
|
StringBuilder builder = new StringBuilder(ROOM_DRAWABLE_PREFIX.length() + roomName.length());
|
||||||
builder.append(ROOM_DRAWABLE_PREFIX);
|
builder.append(ROOM_DRAWABLE_PREFIX);
|
||||||
int size = roomName.length();
|
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>
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto" >
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/filter"
|
android:id="@+id/filter"
|
||||||
|
@ -13,5 +13,10 @@
|
||||||
android:title="@string/upcoming_only"/>
|
android:title="@string/upcoming_only"/>
|
||||||
</menu>
|
</menu>
|
||||||
</item>
|
</item>
|
||||||
|
<item
|
||||||
|
android:id="@+id/export_bookmarks"
|
||||||
|
android:icon="@drawable/ic_file_upload_white_24dp"
|
||||||
|
android:title="@string/export_bookmarks"
|
||||||
|
app:showAsAction="ifRoom"/>
|
||||||
|
|
||||||
</menu>
|
</menu>
|
|
@ -28,6 +28,8 @@
|
||||||
<!-- Bookmarks -->
|
<!-- Bookmarks -->
|
||||||
<string name="filter">Filter</string>
|
<string name="filter">Filter</string>
|
||||||
<string name="upcoming_only">Upcoming only</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="no_bookmark">No bookmark.</string>
|
||||||
<string name="remove_bookmarks">Remove bookmarks</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>
|
<string name="bookmark_conflict_content_description">%1$s\n Other bookmarks are scheduled at the same time.</string>
|
||||||
|
|
Loading…
Reference in a new issue