public static class Blocks implements BlocksColumns, BaseColumns {
    public static final Uri CONTENT_URI =
            BASE_CONTENT_URI.buildUpon().appendPath(PATH_BLOCKS).build();

    public static final String CONTENT_TYPE =
            "vnd.android.cursor.dir/vnd.dccsched.block";
    public static final String CONTENT_ITEM_TYPE =
            "vnd.android.cursor.item/vnd.dccsched.block";

    /** Count of {@link Sessions} inside given block. */
    public static final String SESSIONS_COUNT = "sessions_count";

    /**
     * Flag indicating that at least one {@link Sessions#SESSION_ID} inside
     * this block has {@link Sessions#STARRED} set.
     */
    public static final String CONTAINS_STARRED = "contains_starred";

    /** Default "ORDER BY" clause. */
    public static final String DEFAULT_SORT = BlocksColumns.BLOCK_START + " ASC, "
            + BlocksColumns.BLOCK_END + " ASC";

    /** Build {@link Uri} for requested {@link #BLOCK_ID}. */
    public static Uri buildBlockUri(String blockId) {
        return CONTENT_URI.buildUpon().appendPath(blockId).build();
    }

    /**
     * Build {@link Uri} that references any {@link Sessions} associated
     * with the requested {@link #BLOCK_ID}.
     */
    public static Uri buildSessionsUri(String blockId) {
        return CONTENT_URI.buildUpon().appendPath(blockId).appendPath(PATH_SESSIONS).build();
    }

    /**
     * Build {@link Uri} that references any {@link Blocks} that occur
     * between the requested time boundaries.
     */
    public static Uri buildBlocksBetweenDirUri(long startTime, long endTime) {
        return CONTENT_URI.buildUpon().appendPath(PATH_BETWEEN).appendPath(
                String.valueOf(startTime)).appendPath(String.valueOf(endTime)).build();
    }

    /** Read {@link #BLOCK_ID} from {@link Blocks} {@link Uri}. */
    public static String getBlockId(Uri uri) {
        return uri.getPathSegments().get(1);
    }

    /**
     * Generate a {@link #BLOCK_ID} that will always match the requested
     * {@link Blocks} details.
     */
    public static String generateBlockId(long startTime, long endTime) {
        startTime /= DateUtils.SECOND_IN_MILLIS;
        endTime /= DateUtils.SECOND_IN_MILLIS;
        return ParserUtils.sanitizeId(startTime + "-" + endTime);
    }
} public static class Notes implements NotesColumns, BaseColumns {
    public static final Uri CONTENT_URI =
            BASE_CONTENT_URI.buildUpon().appendPath(PATH_NOTES).build();
    public static final Uri CONTENT_EXPORT_URI =
            CONTENT_URI.buildUpon().appendPath(PATH_EXPORT).build();

    /** {@link Sessions#SESSION_ID} that this note references. */
    public static final String SESSION_ID = "session_id";

    /** Default "ORDER BY" clause. */
    public static final String DEFAULT_SORT = NotesColumns.NOTE_TIME + " DESC";

    public static final String CONTENT_TYPE =
            "vnd.android.cursor.dir/vnd.dccsched.note";
    public static final String CONTENT_ITEM_TYPE =
            "vnd.android.cursor.item/vnd.dccsched.note";

    public static Uri buildNoteUri(long noteId) {
        return ContentUris.withAppendedId(CONTENT_URI, noteId);
    }

    public static long getNoteId(Uri uri) {
        return ContentUris.parseId(uri);
    }
} public static class Rooms implements RoomsColumns, BaseColumns {
    public static final Uri CONTENT_URI =
            BASE_CONTENT_URI.buildUpon().appendPath(PATH_ROOMS).build();

    public static final String CONTENT_TYPE =
            "vnd.android.cursor.dir/vnd.dccsched.room";
    public static final String CONTENT_ITEM_TYPE =
            "vnd.android.cursor.item/vnd.dccsched.room";

    /** Default "ORDER BY" clause. */
    public static final String DEFAULT_SORT = RoomsColumns.ROOM_FLOOR + " ASC, "
            + RoomsColumns.ROOM_NAME + " COLLATE NOCASE ASC";

    /** Build {@link Uri} for requested {@link #ROOM_ID}. */
    public static Uri buildRoomUri(String roomId) {
        return CONTENT_URI.buildUpon().appendPath(roomId).build();
    }

    /**
     * Build {@link Uri} that references any {@link Sessions} associated
     * with the requested {@link #ROOM_ID}.
     */
    public static Uri buildSessionsDirUri(String roomId) {
        return CONTENT_URI.buildUpon().appendPath(roomId).appendPath(PATH_SESSIONS).build();
    }

    /** Read {@link #ROOM_ID} from {@link Rooms} {@link Uri}. */
    public static String getRoomId(Uri uri) {
        return uri.getPathSegments().get(1);
    }

    /**
     * Generate a {@link #ROOM_ID} that will always match the requested
     * {@link Rooms} details.
     */
    public static String generateRoomId(String room) {
        return ParserUtils.sanitizeId(room);
    }
} public static class SearchSuggest {
    public static final Uri CONTENT_URI =
            BASE_CONTENT_URI.buildUpon().appendPath(PATH_SEARCH_SUGGEST).build();

    public static final String DEFAULT_SORT = SearchManager.SUGGEST_COLUMN_TEXT_1
            + " COLLATE NOCASE ASC";
} public static class Sessions implements SessionsColumns, BlocksColumns, RoomsColumns,
        SyncColumns, BaseColumns {
    public static final Uri CONTENT_URI =
            BASE_CONTENT_URI.buildUpon().appendPath(PATH_SESSIONS).build();
    public static final Uri CONTENT_STARRED_URI =
            CONTENT_URI.buildUpon().appendPath(PATH_STARRED).build();

    public static final String CONTENT_TYPE =
            "vnd.android.cursor.dir/vnd.dccsched.session";
    public static final String CONTENT_ITEM_TYPE =
            "vnd.android.cursor.item/vnd.dccsched.session";

    public static final String BLOCK_ID = "block_id";
    public static final String ROOM_ID = "room_id";

    public static final String SEARCH_SNIPPET = "search_snippet";

    // TODO: shortcut primary track to offer sub-sorting here
    /** Default "ORDER BY" clause. */
    public static final String DEFAULT_SORT = BlocksColumns.BLOCK_START + " ASC,"
            + SessionsColumns.TITLE + " COLLATE NOCASE ASC";

    /** Build {@link Uri} for requested {@link #SESSION_ID}. */
    public static Uri buildSessionUri(String sessionId) {
        return CONTENT_URI.buildUpon().appendPath(sessionId).build();
    }

    /**
     * Build {@link Uri} that references any {@link Speakers} associated
     * with the requested {@link #SESSION_ID}.
     */
    public static Uri buildSpeakersDirUri(String sessionId) {
        return CONTENT_URI.buildUpon().appendPath(sessionId).appendPath(PATH_SPEAKERS).build();
    }

    /**
     * Build {@link Uri} that references any {@link Tracks} associated with
     * the requested {@link #SESSION_ID}.
     */
    public static Uri buildTracksDirUri(String sessionId) {
        return CONTENT_URI.buildUpon().appendPath(sessionId).appendPath(PATH_TRACKS).build();
    }

    /**
     * Build {@link Uri} that references any {@link Notes} associated with
     * the requested {@link #SESSION_ID}.
     */
    public static Uri buildNotesDirUri(String sessionId) {
        return CONTENT_URI.buildUpon().appendPath(sessionId).appendPath(PATH_NOTES).build();
    }

    public static Uri buildSessionsAtDirUri(long time) {
        return CONTENT_URI.buildUpon().appendPath(PATH_AT).appendPath(String.valueOf(time))
                .build();
    }

    public static Uri buildSearchUri(String query) {
        return CONTENT_URI.buildUpon().appendPath(PATH_SEARCH).appendPath(query).build();
    }

    public static boolean isSearchUri(Uri uri) {
        return PATH_SEARCH.equals(uri.getPathSegments().get(1));
    }

    /** Read {@link #SESSION_ID} from {@link Sessions} {@link Uri}. */
    public static String getSessionId(Uri uri) {
        return uri.getPathSegments().get(1);
    }

    public static String getSearchQuery(Uri uri) {
        return uri.getPathSegments().get(2);
    }

    /**
     * Generate a {@link #SESSION_ID} that will always match the requested
     * {@link Sessions} details.
     */
    public static String generateSessionId(String title) {
        return ParserUtils.sanitizeId(title);
    }
} public static class Speakers implements SpeakersColumns, SyncColumns, BaseColumns {
    public static final Uri CONTENT_URI =
            BASE_CONTENT_URI.buildUpon().appendPath(PATH_SPEAKERS).build();

    public static final String CONTENT_TYPE =
            "vnd.android.cursor.dir/vnd.dccsched.speaker";
    public static final String CONTENT_ITEM_TYPE =
            "vnd.android.cursor.item/vnd.dccsched.speaker";

    /** Default "ORDER BY" clause. */
    public static final String DEFAULT_SORT = SpeakersColumns.SPEAKER_NAME
            + " COLLATE NOCASE ASC";

    /** Build {@link Uri} for requested {@link #SPEAKER_ID}. */
    public static Uri buildSpeakerUri(String speakerId) {
        return CONTENT_URI.buildUpon().appendPath(speakerId).build();
    }

    /**
     * Build {@link Uri} that references any {@link Sessions} associated
     * with the requested {@link #SPEAKER_ID}.
     */
    public static Uri buildSessionsDirUri(String speakerId) {
        return CONTENT_URI.buildUpon().appendPath(speakerId).appendPath(PATH_SESSIONS).build();
    }

    /** Read {@link #SPEAKER_ID} from {@link Speakers} {@link Uri}. */
    public static String getSpeakerId(Uri uri) {
        return uri.getPathSegments().get(1);
    }

    /**
     * Generate a {@link #SPEAKER_ID} that will always match the requested
     * {@link Speakers} details.
     */
    public static String generateSpeakerId(String speakerLdap) {
        return ParserUtils.sanitizeId(speakerLdap);
    }
} public static class Tracks implements TracksColumns, BaseColumns {
    public static final Uri CONTENT_URI =
            BASE_CONTENT_URI.buildUpon().appendPath(PATH_TRACKS).build();

    public static final String CONTENT_TYPE =
            "vnd.android.cursor.dir/vnd.dccsched.track";
    public static final String CONTENT_ITEM_TYPE =
            "vnd.android.cursor.item/vnd.dccsched.track";

    /** Count of {@link Sessions} inside given track. */
    public static final String SESSIONS_COUNT = "sessions_count";
    /** Count of {@link Vendors} inside given track. */
    public static final String VENDORS_COUNT = "vendors_count";

    /** Default "ORDER BY" clause. */
    public static final String DEFAULT_SORT = TracksColumns.TRACK_NAME + " ASC";

    /** Build {@link Uri} for requested {@link #TRACK_ID}. */
    public static Uri buildTrackUri(String trackId) {
        return CONTENT_URI.buildUpon().appendPath(trackId).build();
    }

    /**
     * Build {@link Uri} that references any {@link Sessions} associated
     * with the requested {@link #TRACK_ID}.
     */
    public static Uri buildSessionsUri(String trackId) {
        return CONTENT_URI.buildUpon().appendPath(trackId).appendPath(PATH_SESSIONS).build();
    }

    /**
     * Build {@link Uri} that references any {@link Vendors} associated with
     * the requested {@link #TRACK_ID}.
     */
    public static Uri buildVendorsUri(String trackId) {
        return CONTENT_URI.buildUpon().appendPath(trackId).appendPath(PATH_VENDORS).build();
    }

    /** Read {@link #TRACK_ID} from {@link Tracks} {@link Uri}. */
    public static String getTrackId(Uri uri) {
        return uri.getPathSegments().get(1);
    }

    /**
     * Generate a {@link #TRACK_ID} that will always match the requested
     * {@link Tracks} details.
     */
    public static String generateTrackId(String title) {
        return ParserUtils.sanitizeId(title);
    }
} public static class Vendors implements VendorsColumns, SyncColumns, BaseColumns {
    public static final Uri CONTENT_URI =
            BASE_CONTENT_URI.buildUpon().appendPath(PATH_VENDORS).build();
    public static final Uri CONTENT_STARRED_URI =
            CONTENT_URI.buildUpon().appendPath(PATH_STARRED).build();

    public static final String CONTENT_TYPE =
            "vnd.android.cursor.dir/vnd.dccsched.vendor";
    public static final String CONTENT_ITEM_TYPE =
            "vnd.android.cursor.item/vnd.dccsched.vendor";

    /** {@link Tracks#TRACK_ID} that this vendor belongs to. */
    public static final String TRACK_ID = "track_id";

    public static final String SEARCH_SNIPPET = "search_snippet";

    /** Default "ORDER BY" clause. */
    public static final String DEFAULT_SORT = VendorsColumns.NAME + " COLLATE NOCASE ASC";

    /** Build {@link Uri} for requested {@link #VENDOR_ID}. */
    public static Uri buildVendorUri(String vendorId) {
        return CONTENT_URI.buildUpon().appendPath(vendorId).build();
    }

    public static Uri buildSearchUri(String query) {
        return CONTENT_URI.buildUpon().appendPath(PATH_SEARCH).appendPath(query).build();
    }

    public static boolean isSearchUri(Uri uri) {
        return PATH_SEARCH.equals(uri.getPathSegments().get(1));
    }

    /** Read {@link #VENDOR_ID} from {@link Vendors} {@link Uri}. */
    public static String getVendorId(Uri uri) {
        return uri.getPathSegments().get(1);
    }

    public static String getSearchQuery(Uri uri) {
        return uri.getPathSegments().get(2);
    }

    /**
     * Generate a {@link #VENDOR_ID} that will always match the requested
     * {@link Vendors} details.
     */
    public static String generateVendorId(String companyLogo) {
        return ParserUtils.sanitizeId(companyLogo);
    }
} interface SessionsSearchColumns {
    String SESSION_ID = "session_id";
    String BODY = "body";
} public interface SessionsSpeakers {
    String SESSION_ID = "session_id";
    String SPEAKER_ID = "speaker_id";
} public interface SessionsTracks {
    String SESSION_ID = "session_id";
    String TRACK_ID = "track_id";
} interface Tables {
    String BLOCKS = "blocks";
    String TRACKS = "tracks";
    String ROOMS = "rooms";
    String SESSIONS = "sessions";
    String SPEAKERS = "speakers";
    String SESSIONS_SPEAKERS = "sessions_speakers";
    String SESSIONS_TRACKS = "sessions_tracks";
    String VENDORS = "vendors";
    String NOTES = "notes";

    String SESSIONS_SEARCH = "sessions_search";
    String VENDORS_SEARCH = "vendors_search";

    String SEARCH_SUGGEST = "search_suggest";

    String SESSIONS_JOIN_BLOCKS_ROOMS = "sessions "
            + "LEFT OUTER JOIN blocks ON sessions.block_id=blocks.block_id "
            + "LEFT OUTER JOIN rooms ON sessions.room_id=rooms.room_id";

    String VENDORS_JOIN_TRACKS = "vendors "
            + "LEFT OUTER JOIN tracks ON vendors.track_id=tracks.track_id";

    String SESSIONS_SPEAKERS_JOIN_SPEAKERS = "sessions_speakers "
            + "LEFT OUTER JOIN speakers ON sessions_speakers.speaker_id=speakers.speaker_id";

    String SESSIONS_SPEAKERS_JOIN_SESSIONS_BLOCKS_ROOMS = "sessions_speakers "
            + "LEFT OUTER JOIN sessions ON sessions_speakers.session_id=sessions.session_id "
            + "LEFT OUTER JOIN blocks ON sessions.block_id=blocks.block_id "
            + "LEFT OUTER JOIN rooms ON sessions.room_id=rooms.room_id";

    String SESSIONS_TRACKS_JOIN_TRACKS = "sessions_tracks "
            + "LEFT OUTER JOIN tracks ON sessions_tracks.track_id=tracks.track_id";

    String SESSIONS_TRACKS_JOIN_SESSIONS_BLOCKS_ROOMS = "sessions_tracks "
            + "LEFT OUTER JOIN sessions ON sessions_tracks.session_id=sessions.session_id "
            + "LEFT OUTER JOIN blocks ON sessions.block_id=blocks.block_id "
            + "LEFT OUTER JOIN rooms ON sessions.room_id=rooms.room_id";

    String SESSIONS_SEARCH_JOIN_SESSIONS_BLOCKS_ROOMS = "sessions_search "
            + "LEFT OUTER JOIN sessions ON sessions_search.session_id=sessions.session_id "
            + "LEFT OUTER JOIN blocks ON sessions.block_id=blocks.block_id "
            + "LEFT OUTER JOIN rooms ON sessions.room_id=rooms.room_id";

    String VENDORS_SEARCH_JOIN_VENDORS_TRACKS = "vendors_search "
            + "LEFT OUTER JOIN vendors ON vendors_search.vendor_id=vendors.vendor_id "
            + "LEFT OUTER JOIN tracks ON vendors.track_id=tracks.track_id";

} interface VendorsSearchColumns {
    String VENDOR_ID = "vendor_id";
    String BODY = "body";
} public class SyncService extends IntentService {
    private static final String TAG = "SyncService";

    public static final String EXTRA_STATUS_RECEIVER =
            "com.google.android.dccsched.extra.STATUS_RECEIVER";

    public static final int STATUS_RUNNING = 0x1;
    public static final int STATUS_ERROR = 0x2;
    public static final int STATUS_FINISHED = 0x3;

    private static final int SECOND_IN_MILLIS = (int) DateUtils.SECOND_IN_MILLIS;

    /** Root worksheet feed for online data source */
    //private static final String WORKSHEETS_URL = "http://spreadsheets.google.com"
     //       + "/feeds/worksheets/t0bDxnEqbFO4XuYpkA070Nw/public/basic";
    //private static final String WORKSHEETS_URL = "http://spreadsheets.google.com/"
	//    + "feeds/worksheets/0An7RnSwRegJfdHdkNnN5TTQ5M29GcUlGV2VJbThxR3c/public/basic";
	private static final String WORKSHEETS_URL = "http://spreadsheets.google.com/"
		+ "feeds/worksheets/twd6syM493oFqIFWeIm8qGw/public/basic";
    private static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
    private static final String ENCODING_GZIP = "gzip";

    private static final int VERSION_NONE = 0;
    private static final int VERSION_CURRENT = 5;

    private LocalExecutor mLocalExecutor;
    private RemoteExecutor mRemoteExecutor;

    public SyncService() {
        super(TAG);
    }

    @Override
    public void onCreate() {
        super.onCreate();

        final HttpClient httpClient = getHttpClient(this);
        final ContentResolver resolver = getContentResolver();

        mLocalExecutor = new LocalExecutor(getResources(), resolver);
        mRemoteExecutor = new RemoteExecutor(httpClient, resolver);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.d(TAG, "onHandleIntent(intent=" + intent.toString() + ")");

        final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_STATUS_RECEIVER);
        if (receiver != null) receiver.send(STATUS_RUNNING, Bundle.EMPTY);

        final Context context = this;
        final SharedPreferences prefs = getSharedPreferences(Prefs.IOSCHED_SYNC,
                Context.MODE_PRIVATE);
        final int localVersion = prefs.getInt(Prefs.LOCAL_VERSION, VERSION_NONE);

        try {
            // Bulk of sync work, performed by executing several fetches from
            // local and online sources.

            final long startLocal = System.currentTimeMillis();
            final boolean localParse = localVersion < VERSION_CURRENT;
            Log.d(TAG, "found localVersion=" + localVersion + " and VERSION_CURRENT="
                    + VERSION_CURRENT);
            if (localParse) {
                // Load static local data
                mLocalExecutor.execute(R.xml.blocks, new LocalBlocksHandler());
                mLocalExecutor.execute(R.xml.rooms, new LocalRoomsHandler());
                mLocalExecutor.execute(R.xml.tracks, new LocalTracksHandler());
                mLocalExecutor.execute(R.xml.search_suggest, new LocalSearchSuggestHandler());
                mLocalExecutor.execute(R.xml.sessions, new LocalSessionsHandler());

                // Parse values from local cache first, since spreadsheet copy
                // or network might be down.
                mLocalExecutor.execute(context, "cache-sessions.xml", new RemoteSessionsHandler());
                mLocalExecutor.execute(context, "cache-speakers.xml", new RemoteSpeakersHandler());
                mLocalExecutor.execute(context, "cache-vendors.xml", new RemoteVendorsHandler());

                // Save local parsed version
                prefs.edit().putInt(Prefs.LOCAL_VERSION, VERSION_CURRENT).commit();
            }
            Log.d(TAG, "local sync took " + (System.currentTimeMillis() - startLocal) + "ms");

            // Always hit remote spreadsheet for any updates
            final long startRemote = System.currentTimeMillis();
            mRemoteExecutor
                    .executeGet(WORKSHEETS_URL, new RemoteWorksheetsHandler(mRemoteExecutor));
            Log.d(TAG, "remote sync took " + (System.currentTimeMillis() - startRemote) + "ms");

        } catch (Exception e) {
            Log.e(TAG, "Problem while syncing", e);

            if (receiver != null) {
                // Pass back error to surface listener
                final Bundle bundle = new Bundle();
                bundle.putString(Intent.EXTRA_TEXT, e.toString());
                receiver.send(STATUS_ERROR, bundle);
            }
        }

        // Announce success to any surface listener
        Log.d(TAG, "sync finished");
        if (receiver != null) receiver.send(STATUS_FINISHED, Bundle.EMPTY);
    }

    /**
     * Generate and return a {@link HttpClient} configured for general use,
     * including setting an application-specific user-agent string.
     */
    public static HttpClient getHttpClient(Context context) {
        final HttpParams params = new BasicHttpParams();

        // Use generous timeouts for slow mobile networks
        HttpConnectionParams.setConnectionTimeout(params, 20 * SECOND_IN_MILLIS);
        HttpConnectionParams.setSoTimeout(params, 20 * SECOND_IN_MILLIS);

        HttpConnectionParams.setSocketBufferSize(params, 8192);
        HttpProtocolParams.setUserAgent(params, buildUserAgent(context));

        final DefaultHttpClient client = new DefaultHttpClient(params);

        client.addRequestInterceptor(new HttpRequestInterceptor() {
            public void process(HttpRequest request, HttpContext context) {
                // Add header to accept gzip content
                if (!request.containsHeader(HEADER_ACCEPT_ENCODING)) {
                    request.addHeader(HEADER_ACCEPT_ENCODING, ENCODING_GZIP);
                }
            }
        });

        client.addResponseInterceptor(new HttpResponseInterceptor() {
            public void process(HttpResponse response, HttpContext context) {
                // Inflate any responses compressed with gzip
                final HttpEntity entity = response.getEntity();
                final Header encoding = entity.getContentEncoding();
                if (encoding != null) {
                    for (HeaderElement element : encoding.getElements()) {
                        if (element.getName().equalsIgnoreCase(ENCODING_GZIP)) {
                            response.setEntity(new InflatingEntity(response.getEntity()));
                            break;
                        }
                    }
                }
            }
        });

        return client;
    }

    /**
     * Build and return a user-agent string that can identify this application
     * to remote servers. Contains the package name and version code.
     */
    private static String buildUserAgent(Context context) {
        try {
            final PackageManager manager = context.getPackageManager();
            final PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0);

            // Some APIs require "(gzip)" in the user-agent string.
            return info.packageName + "/" + info.versionName
                    + " (" + info.versionCode + ") (gzip)";
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    /**
     * Simple {@link HttpEntityWrapper} that inflates the wrapped
     * {@link HttpEntity} by passing it through {@link GZIPInputStream}.
     */
    private static class InflatingEntity extends HttpEntityWrapper {
        public InflatingEntity(HttpEntity wrapped) {
            super(wrapped);
        }

        @Override
        public InputStream getContent() throws IOException {
            return new GZIPInputStream(wrappedEntity.getContent());
        }

        @Override
        public long getContentLength() {
            return -1;
        }
    }

    private interface Prefs {
        String IOSCHED_SYNC = "dccsched_sync";
        String LOCAL_VERSION = "local_version";
    }
} public class NotesExporter {

    public static File writeExportedNotes(Context context) throws IOException {
        // TODO: allow customization by accepting dir uri and output file
        final ContentResolver resolver = context.getContentResolver();
        final File notesFile = context.getFileStreamPath("notes.xml");
        final BufferedWriter out = new BufferedWriter(new FileWriter(notesFile));
        final XmlSerializer serializer = Xml.newSerializer();

        serializer.setOutput(out);
        serializer.startDocument("UTF-8", true);
        serializer.startTag("", Tags.NOTES);

        final Time time = new Time();

        final Uri notesUri = Notes.CONTENT_URI;
        final Cursor cursor = resolver.query(notesUri, NotesQuery.PROJECTION, null, null,
                Notes.DEFAULT_SORT);
        try {
            while (cursor.moveToNext()) {
                serializer.startTag("", Tags.NOTE);
                {
                    serializer.startTag("", Tags.SESSION_ID);
                    serializer.text(cursor.getString(NotesQuery.SESSION_ID));
                    serializer.endTag("", Tags.SESSION_ID);
                }
                {
                    serializer.startTag("", Tags.TIME);
                    time.set(cursor.getLong(NotesQuery.NOTE_TIME));
                    final String timeString = time.format3339(false);
                    serializer.text(timeString);
                    serializer.endTag("", Tags.TIME);
                }
                {
                    serializer.startTag("", Tags.CONTENT);
                    serializer.text(cursor.getString(NotesQuery.NOTE_CONTENT));
                    serializer.endTag("", Tags.CONTENT);
                }
                serializer.endTag("", Tags.NOTE);
            }
        } finally {
            cursor.close();
        }

        serializer.endTag("", Tags.NOTES);
        serializer.endDocument();

        out.flush();
        out.close();

        return notesFile;
    }

    private interface Tags {
        String NOTES = "notes";
        String NOTE = "note";
        String SESSION_ID = "sessionId";
        String CONTENT = "content";
        String TIME = "time";
    }

    private interface NotesQuery {
        String[] PROJECTION = {
                Notes.SESSION_ID,
                Notes.NOTE_CONTENT,
                Notes.NOTE_TIME,
        };

        int SESSION_ID = 0;
        int NOTE_CONTENT = 1;
        int NOTE_TIME = 2;
    }
} public class SelectionBuilder {
    private static final String TAG = "SelectionBuilder";
    private static final boolean LOGV = false;

    private String mTable = null;
    private Map<String, String> mProjectionMap = Maps.newHashMap();
    private StringBuilder mSelection = new StringBuilder();
    private ArrayList<String> mSelectionArgs = Lists.newArrayList();

    /**
     * Reset any internal state, allowing this builder to be recycled.
     */
    public SelectionBuilder reset() {
        mTable = null;
        mSelection.setLength(0);
        mSelectionArgs.clear();
        return this;
    }

    /**
     * Append the given selection clause to the internal state. Each clause is
     * surrounded with parenthesis and combined using {@code AND}.
     */
    public SelectionBuilder where(String selection, String... selectionArgs) {
        if (TextUtils.isEmpty(selection)) {
            if (selectionArgs != null && selectionArgs.length > 0) {
                throw new IllegalArgumentException(
                        "Valid selection required when including arguments=");
            }

            // Shortcut when clause is empty
            return this;
        }

        if (mSelection.length() > 0) {
            mSelection.append(" AND ");
        }

        mSelection.append("(").append(selection).append(")");
        if (selectionArgs != null) {
            for (String arg : selectionArgs) {
                mSelectionArgs.add(arg);
            }
        }

        return this;
    }

    public SelectionBuilder table(String table) {
        mTable = table;
        return this;
    }

    private void assertTable() {
        if (mTable == null) {
            throw new IllegalStateException("Table not specified");
        }
    }

    public SelectionBuilder mapToTable(String column, String table) {
        mProjectionMap.put(column, table + "." + column);
        return this;
    }

    public SelectionBuilder map(String fromColumn, String toClause) {
        mProjectionMap.put(fromColumn, toClause + " AS " + fromColumn);
        return this;
    }

    /**
     * Return selection string for current internal state.
     *
     * @see #getSelectionArgs()
     */
    public String getSelection() {
        return mSelection.toString();
    }

    /**
     * Return selection arguments for current internal state.
     *
     * @see #getSelection()
     */
    public String[] getSelectionArgs() {
        return mSelectionArgs.toArray(new String[mSelectionArgs.size()]);
    }

    private void mapColumns(String[] columns) {
        for (int i = 0; i < columns.length; i++) {
            final String target = mProjectionMap.get(columns[i]);
            if (target != null) {
                columns[i] = target;
            }
        }
    }

    @Override
    public String toString() {
        return "SelectionBuilder[table=" + mTable + ", selection=" + getSelection()
                + ", selectionArgs=" + Arrays.toString(getSelectionArgs()) + "]";
    }

    /**
     * Execute query using the current internal state as {@code WHERE} clause.
     */
    public Cursor query(SQLiteDatabase db, String[] columns, String orderBy) {
        return query(db, columns, null, null, orderBy, null);
    }

    /**
     * Execute query using the current internal state as {@code WHERE} clause.
     */
    public Cursor query(SQLiteDatabase db, String[] columns, String groupBy,
            String having, String orderBy, String limit) {
        assertTable();
        if (columns != null) mapColumns(columns);
        if (LOGV) Log.v(TAG, "query(columns=" + Arrays.toString(columns) + ") " + this);
        return db.query(mTable, columns, getSelection(), getSelectionArgs(), groupBy, having,
                orderBy, limit);
    }

    /**
     * Execute update using the current internal state as {@code WHERE} clause.
     */
    public int update(SQLiteDatabase db, ContentValues values) {
        assertTable();
        if (LOGV) Log.v(TAG, "update() " + this);
        return db.update(mTable, values, getSelection(), getSelectionArgs());
    }

    /**
     * Execute delete using the current internal state as {@code WHERE} clause.
     */
    public int delete(SQLiteDatabase db) {
        assertTable();
        if (LOGV) Log.v(TAG, "delete() " + this);
        return db.delete(mTable, getSelection(), getSelectionArgs());
    }
}
import com.underhilllabs.dccsched.provider.ScheduleContract.Blocks;
import com.underhilllabs.dccsched.provider.ScheduleContract.Notes;
import com.underhilllabs.dccsched.provider.ScheduleContract.Rooms;
import com.underhilllabs.dccsched.provider.ScheduleContract.SearchSuggest;
import com.underhilllabs.dccsched.provider.ScheduleContract.Sessions;
import com.underhilllabs.dccsched.provider.ScheduleContract.Speakers;
import com.underhilllabs.dccsched.provider.ScheduleContract.Tracks;
import com.underhilllabs.dccsched.provider.ScheduleContract.Vendors;
import com.underhilllabs.dccsched.provider.ScheduleDatabase.SessionsSearchColumns;
import com.underhilllabs.dccsched.provider.ScheduleDatabase.SessionsSpeakers;
import com.underhilllabs.dccsched.provider.ScheduleDatabase.SessionsTracks;
import com.underhilllabs.dccsched.provider.ScheduleDatabase.Tables;
import com.underhilllabs.dccsched.provider.ScheduleDatabase.VendorsSearchColumns;
import com.underhilllabs.dccsched.service.SyncService;
import com.underhilllabs.dccsched.util.NotesExporter;
import com.underhilllabs.dccsched.util.SelectionBuilder;
import android.app.Activity;
import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
import android.provider.OpenableColumns;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
            }
            case NOTES: {
                return builder.table(Tables.NOTES);
            }
            case NOTES_ID: {
                final String noteId = uri.getPathSegments().get(1);
                return builder.table(Tables.NOTES)
                        .where(Notes._ID + "=?", noteId);
            }
            case SEARCH_SUGGEST: {
                return builder.table(Tables.SEARCH_SUGGEST);
            }
            default: {
                throw new UnsupportedOperationException("Unknown uri: " + uri);
            }
        }
    }

    /**
     * Build an advanced {@link SelectionBuilder} to match the requested
     * {@link Uri}. This is usually only used by {@link #query}, since it
     * performs table joins useful for {@link Cursor} data.
     */
    private SelectionBuilder buildExpandedSelection(Uri uri, int match) {
        final SelectionBuilder builder = new SelectionBuilder();
        switch (match) {
            case BLOCKS: {
                return builder.table(Tables.BLOCKS);
            }
            case BLOCKS_BETWEEN: {
                final List<String> segments = uri.getPathSegments();
                final String startTime = segments.get(2);
                final String endTime = segments.get(3);
                return builder.table(Tables.BLOCKS)
                        .map(Blocks.SESSIONS_COUNT, Subquery.BLOCK_SESSIONS_COUNT)
                        .map(Blocks.CONTAINS_STARRED, Subquery.BLOCK_CONTAINS_STARRED)
                        .where(Blocks.BLOCK_START + ">=?", startTime)
                        .where(Blocks.BLOCK_START + "<=?", endTime);
            }
            case BLOCKS_ID: {
                final String blockId = Blocks.getBlockId(uri);
                return builder.table(Tables.BLOCKS)
                        .map(Blocks.SESSIONS_COUNT, Subquery.BLOCK_SESSIONS_COUNT)
                        .map(Blocks.CONTAINS_STARRED, Subquery.BLOCK_CONTAINS_STARRED)
                        .where(Blocks.BLOCK_ID + "=?", blockId);
            }
            case BLOCKS_ID_SESSIONS: {
                final String blockId = Blocks.getBlockId(uri);
                return builder.table(Tables.SESSIONS_JOIN_BLOCKS_ROOMS)
                        .map(Blocks.SESSIONS_COUNT, Subquery.BLOCK_SESSIONS_COUNT)
                        .map(Blocks.CONTAINS_STARRED, Subquery.BLOCK_CONTAINS_STARRED)
                        .mapToTable(Sessions._ID, Tables.SESSIONS)
                        .mapToTable(Sessions.SESSION_ID, Tables.SESSIONS)
                        .mapToTable(Sessions.BLOCK_ID, Tables.SESSIONS)
                        .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS)
                        .where(Qualified.SESSIONS_BLOCK_ID + "=?", blockId);
            }
            case TRACKS: {
                return builder.table(Tables.TRACKS)
                        .map(Tracks.SESSIONS_COUNT, Subquery.TRACK_SESSIONS_COUNT)
                        .map(Tracks.VENDORS_COUNT, Subquery.TRACK_VENDORS_COUNT);
            }
            case TRACKS_ID: {
                final String trackId = Tracks.getTrackId(uri);
                return builder.table(Tables.TRACKS)
                        .where(Tracks.TRACK_ID + "=?", trackId);
            }
            case TRACKS_ID_SESSIONS: {
                final String trackId = Tracks.getTrackId(uri);
                return builder.table(Tables.SESSIONS_TRACKS_JOIN_SESSIONS_BLOCKS_ROOMS)
                        .mapToTable(Sessions._ID, Tables.SESSIONS)
                        .mapToTable(Sessions.SESSION_ID, Tables.SESSIONS)
                        .mapToTable(Sessions.BLOCK_ID, Tables.SESSIONS)
                        .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS)
                        .where(Qualified.SESSIONS_TRACKS_TRACK_ID + "=?", trackId);
            }
            case TRACKS_ID_VENDORS: {
                final String trackId = Tracks.getTrackId(uri);
                return builder.table(Tables.VENDORS_JOIN_TRACKS)
                        .mapToTable(Vendors._ID, Tables.VENDORS)
                        .mapToTable(Vendors.TRACK_ID, Tables.VENDORS)
                        .where(Qualified.VENDORS_TRACK_ID + "=?", trackId);
            }
            case ROOMS: {
                return builder.table(Tables.ROOMS);
            }
            case ROOMS_ID: {
                final String roomId = Rooms.getRoomId(uri);
                return builder.table(Tables.ROOMS)
                        .where(Rooms.ROOM_ID + "=?", roomId);
            }
            case ROOMS_ID_SESSIONS: {
                final String roomId = Rooms.getRoomId(uri);
                return builder.table(Tables.SESSIONS_JOIN_BLOCKS_ROOMS)
                        .mapToTable(Sessions._ID, Tables.SESSIONS)
                        .mapToTable(Sessions.BLOCK_ID, Tables.SESSIONS)
                        .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS)
                        .where(Qualified.SESSIONS_ROOM_ID + "=?", roomId);
            }
            case SESSIONS: {
                return builder.table(Tables.SESSIONS_JOIN_BLOCKS_ROOMS)
                        .mapToTable(Sessions._ID, Tables.SESSIONS)
                        .mapToTable(Sessions.BLOCK_ID, Tables.SESSIONS)
                        .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS);
            }
            case SESSIONS_STARRED: {
                return builder.table(Tables.SESSIONS_JOIN_BLOCKS_ROOMS)
                        .mapToTable(Sessions._ID, Tables.SESSIONS)
                        .mapToTable(Sessions.BLOCK_ID, Tables.SESSIONS)
                        .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS)
                        .where(Sessions.STARRED + "=1");
            }
            case SESSIONS_SEARCH: {
                final String query = Sessions.getSearchQuery(uri);
                return builder.table(Tables.SESSIONS_SEARCH_JOIN_SESSIONS_BLOCKS_ROOMS)
                        .map(Sessions.SEARCH_SNIPPET, Subquery.SESSIONS_SNIPPET)
                        .mapToTable(Sessions._ID, Tables.SESSIONS)
                        .mapToTable(Sessions.SESSION_ID, Tables.SESSIONS)
                        .mapToTable(Sessions.BLOCK_ID, Tables.SESSIONS)
                        .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS)
                        .where(SessionsSearchColumns.BODY + " MATCH ?", query);