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