/* * Copyright 2014 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.samples.apps.iosched.provider; import com.google.samples.apps.iosched.appwidget.ScheduleWidgetProvider; import com.google.samples.apps.iosched.provider.ScheduleContract.Announcements; import com.google.samples.apps.iosched.provider.ScheduleContract.PeopleIveMet; import com.google.samples.apps.iosched.provider.ScheduleContract.Blocks; import com.google.samples.apps.iosched.provider.ScheduleContract.Experts; import com.google.samples.apps.iosched.provider.ScheduleContract.Feedback; import com.google.samples.apps.iosched.provider.ScheduleContract.MapMarkers; import com.google.samples.apps.iosched.provider.ScheduleContract.MapTiles; import com.google.samples.apps.iosched.provider.ScheduleContract.Rooms; import com.google.samples.apps.iosched.provider.ScheduleContract.SearchSuggest; import com.google.samples.apps.iosched.provider.ScheduleContract.Sessions; import com.google.samples.apps.iosched.provider.ScheduleContract.Speakers; import com.google.samples.apps.iosched.provider.ScheduleContract.Tags; import com.google.samples.apps.iosched.provider.ScheduleDatabase.SessionsSearchColumns; import com.google.samples.apps.iosched.provider.ScheduleDatabase.SessionsSpeakers; import com.google.samples.apps.iosched.provider.ScheduleDatabase.Tables; import com.google.samples.apps.iosched.util.SelectionBuilder; import android.app.Activity; import android.app.SearchManager; import android.content.*; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; import com.google.samples.apps.iosched.Config; import com.google.samples.apps.iosched.appwidget.ScheduleWidgetProvider; import com.google.samples.apps.iosched.provider.ScheduleContract.*; import com.google.samples.apps.iosched.provider.ScheduleDatabase.SessionsSearchColumns; import com.google.samples.apps.iosched.provider.ScheduleDatabase.SessionsSpeakers; import com.google.samples.apps.iosched.provider.ScheduleDatabase.Tables; import com.google.samples.apps.iosched.util.AccountUtils; import com.google.samples.apps.iosched.util.SelectionBuilder; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static com.google.samples.apps.iosched.util.LogUtils.LOGD; import static com.google.samples.apps.iosched.util.LogUtils.LOGV; import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag; /** * Provider that stores {@link ScheduleContract} data. Data is usually inserted * by {@link com.google.samples.apps.iosched.sync.SyncHelper}, and queried by various * {@link Activity} instances. */ public class ScheduleProvider extends ContentProvider { private static final String TAG = makeLogTag(ScheduleProvider.class); private ScheduleDatabase mOpenHelper; private static final UriMatcher sUriMatcher = buildUriMatcher(); private static final int BLOCKS = 100; private static final int BLOCKS_BETWEEN = 101; private static final int BLOCKS_ID = 102; private static final int TAGS = 200; private static final int TAGS_ID = 201; private static final int ROOMS = 300; private static final int ROOMS_ID = 301; private static final int ROOMS_ID_SESSIONS = 302; private static final int SESSIONS = 400; private static final int SESSIONS_MY_SCHEDULE = 401; private static final int SESSIONS_SEARCH = 403; private static final int SESSIONS_AT = 404; private static final int SESSIONS_ID = 405; private static final int SESSIONS_ID_SPEAKERS = 406; private static final int SESSIONS_ID_TAGS = 407; private static final int SESSIONS_ROOM_AFTER = 408; private static final int SESSIONS_UNSCHEDULED = 409; private static final int SESSIONS_COUNTER = 410; private static final int SPEAKERS = 500; private static final int SPEAKERS_ID = 501; private static final int SPEAKERS_ID_SESSIONS = 502; private static final int MY_SCHEDULE = 600; private static final int ANNOUNCEMENTS = 700; private static final int ANNOUNCEMENTS_ID = 701; private static final int SEARCH_SUGGEST = 800; private static final int SEARCH_INDEX = 801; private static final int MAPMARKERS = 900; private static final int MAPMARKERS_FLOOR = 901; private static final int MAPMARKERS_ID = 902; private static final int MAPTILES = 1000; private static final int MAPTILES_FLOOR = 1001; private static final int FEEDBACK_ALL = 1002; private static final int FEEDBACK_FOR_SESSION = 1003; private static final int EXPERTS = 1100; private static final int EXPERTS_ID = 1101; private static final int HASHTAGS = 1200; private static final int HASHTAGS_NAME = 1201; private static final int PEOPLE_IVE_MET = 1250; private static final int PEOPLE_IVE_MET_ID = 1251; private static final int VIDEOS = 1300; private static final int VIDEOS_ID = 1301; private static final int PARTNERS = 1400; private static final int PARTNERS_ID = 1401; /** * Build and return a {@link UriMatcher} that catches all {@link Uri} * variations supported by this {@link ContentProvider}. */ private static UriMatcher buildUriMatcher() { final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); final String authority = ScheduleContract.CONTENT_AUTHORITY; matcher.addURI(authority, "blocks", BLOCKS); matcher.addURI(authority, "blocks/between/*/*", BLOCKS_BETWEEN); matcher.addURI(authority, "blocks/*", BLOCKS_ID); matcher.addURI(authority, "tags", TAGS); matcher.addURI(authority, "tags/*", TAGS_ID); matcher.addURI(authority, "rooms", ROOMS); matcher.addURI(authority, "rooms/*", ROOMS_ID); matcher.addURI(authority, "rooms/*/sessions", ROOMS_ID_SESSIONS); matcher.addURI(authority, "sessions", SESSIONS); matcher.addURI(authority, "sessions/my_schedule", SESSIONS_MY_SCHEDULE); matcher.addURI(authority, "sessions/search/*", SESSIONS_SEARCH); matcher.addURI(authority, "sessions/at/*", SESSIONS_AT); matcher.addURI(authority, "sessions/unscheduled/*", SESSIONS_UNSCHEDULED); matcher.addURI(authority, "sessions/room/*/after/*", SESSIONS_ROOM_AFTER); matcher.addURI(authority, "sessions/counter", SESSIONS_COUNTER); matcher.addURI(authority, "sessions/*", SESSIONS_ID); matcher.addURI(authority, "sessions/*/speakers", SESSIONS_ID_SPEAKERS); matcher.addURI(authority, "sessions/*/tags", SESSIONS_ID_TAGS); matcher.addURI(authority, "my_schedule", MY_SCHEDULE); matcher.addURI(authority, "speakers", SPEAKERS); matcher.addURI(authority, "speakers/*", SPEAKERS_ID); matcher.addURI(authority, "speakers/*/sessions", SPEAKERS_ID_SESSIONS); matcher.addURI(authority, "announcements", ANNOUNCEMENTS); matcher.addURI(authority, "announcements/*", ANNOUNCEMENTS_ID); matcher.addURI(authority, "search_suggest_query", SEARCH_SUGGEST); matcher.addURI(authority, "search_index", SEARCH_INDEX); // 'update' only matcher.addURI(authority, "mapmarkers", MAPMARKERS); matcher.addURI(authority, "mapmarkers/floor/*", MAPMARKERS_FLOOR); matcher.addURI(authority, "mapmarkers/*", MAPMARKERS_ID); matcher.addURI(authority, "maptiles", MAPTILES); matcher.addURI(authority, "maptiles/*", MAPTILES_FLOOR); matcher.addURI(authority, "feedback/*", FEEDBACK_FOR_SESSION); matcher.addURI(authority, "feedback*", FEEDBACK_ALL); matcher.addURI(authority, "feedback", FEEDBACK_ALL); matcher.addURI(authority, "experts/", EXPERTS); matcher.addURI(authority, "experts/*", EXPERTS_ID); matcher.addURI(authority, "hashtags", HASHTAGS); matcher.addURI(authority, "hashtags/*", HASHTAGS_NAME); matcher.addURI(authority, "people_ive_met/", PEOPLE_IVE_MET); matcher.addURI(authority, "people_ive_met/*", PEOPLE_IVE_MET_ID); matcher.addURI(authority, "videos", VIDEOS); matcher.addURI(authority, "videos/*", VIDEOS_ID); matcher.addURI(authority, "partners", PARTNERS); matcher.addURI(authority, "partners/*", PARTNERS_ID); return matcher; } @Override public boolean onCreate() { mOpenHelper = new ScheduleDatabase(getContext()); return true; } private void deleteDatabase() { // TODO: wait for content provider operations to finish, then tear down mOpenHelper.close(); Context context = getContext(); ScheduleDatabase.deleteDatabase(context); mOpenHelper = new ScheduleDatabase(getContext()); } /** {@inheritDoc} */ @Override public String getType(Uri uri) { final int match = sUriMatcher.match(uri); switch (match) { case BLOCKS: return Blocks.CONTENT_TYPE; case BLOCKS_BETWEEN: return Blocks.CONTENT_TYPE; case BLOCKS_ID: return Blocks.CONTENT_ITEM_TYPE; case TAGS: return Tags.CONTENT_TYPE; case TAGS_ID: return Tags.CONTENT_TYPE; case ROOMS: return Rooms.CONTENT_TYPE; case ROOMS_ID: return Rooms.CONTENT_ITEM_TYPE; case ROOMS_ID_SESSIONS: return Sessions.CONTENT_TYPE; case SESSIONS: return Sessions.CONTENT_TYPE; case SESSIONS_MY_SCHEDULE: return Sessions.CONTENT_TYPE; case SESSIONS_UNSCHEDULED: return Sessions.CONTENT_TYPE; case SESSIONS_SEARCH: return Sessions.CONTENT_TYPE; case SESSIONS_AT: return Sessions.CONTENT_TYPE; case SESSIONS_ID: return Sessions.CONTENT_ITEM_TYPE; case SESSIONS_ID_SPEAKERS: return Speakers.CONTENT_TYPE; case SESSIONS_ID_TAGS: return Tags.CONTENT_TYPE; case SESSIONS_ROOM_AFTER: return Sessions.CONTENT_TYPE; case MY_SCHEDULE: return MySchedule.CONTENT_TYPE; case SPEAKERS: return Speakers.CONTENT_TYPE; case SPEAKERS_ID: return Speakers.CONTENT_ITEM_TYPE; case SPEAKERS_ID_SESSIONS: return Sessions.CONTENT_TYPE; case ANNOUNCEMENTS: return Announcements.CONTENT_TYPE; case ANNOUNCEMENTS_ID: return Announcements.CONTENT_ITEM_TYPE; case MAPMARKERS: return MapMarkers.CONTENT_TYPE; case MAPMARKERS_FLOOR: return MapMarkers.CONTENT_TYPE; case MAPMARKERS_ID: return MapMarkers.CONTENT_ITEM_TYPE; case MAPTILES: return MapTiles.CONTENT_TYPE; case MAPTILES_FLOOR: return MapTiles.CONTENT_ITEM_TYPE; case FEEDBACK_FOR_SESSION: return Feedback.CONTENT_ITEM_TYPE; case FEEDBACK_ALL: return Feedback.CONTENT_TYPE; case EXPERTS: return Experts.CONTENT_TYPE; case EXPERTS_ID: return Experts.CONTENT_ITEM_TYPE; case PEOPLE_IVE_MET: return ScheduleContract.PeopleIveMet.CONTENT_TYPE; case PEOPLE_IVE_MET_ID: return ScheduleContract.PeopleIveMet.CONTENT_ITEM_TYPE; case HASHTAGS: return Hashtags.CONTENT_TYPE; case HASHTAGS_NAME: return Hashtags.CONTENT_ITEM_TYPE; case VIDEOS: return Videos.CONTENT_TYPE; case VIDEOS_ID: return Videos.CONTENT_ITEM_TYPE; case PARTNERS: return Partners.CONTENT_TYPE; case PARTNERS_ID: return Partners.CONTENT_ITEM_TYPE; default: throw new UnsupportedOperationException("Unknown uri: " + uri); } } /** Returns a tuple of question marks. For example, if count is 3, returns "(?,?,?)". */ private String makeQuestionMarkTuple(int count) { if (count < 1) { return "()"; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("(?"); for (int i = 1; i < count; i++) { stringBuilder.append(",?"); } stringBuilder.append(")"); return stringBuilder.toString(); } /** Adds the tags filter query parameter to the given builder. */ private void addTagsFilter(SelectionBuilder builder, String tagsFilter) { // Note: for context, remember that session queries are done on a join of sessions // and the sessions_tags relationship table, and are GROUP'ed BY the session ID. String[] requiredTags = tagsFilter.split(","); if (requiredTags.length == 0) { // filtering by 0 tags -- no-op return; } else if (requiredTags.length == 1) { // filtering by only one tag, so a simple WHERE clause suffices builder.where(Tags.TAG_ID + "=?", requiredTags[0]); } else { // Filtering by multiple tags, so we must add a WHERE clause with an IN operator, // and add a HAVING statement to exclude groups that fall short of the number // of required tags. For example, if requiredTags is { "X", "Y", "Z" }, and a certain // session only has tags "X" and "Y", it will be excluded by the HAVING statement. String questionMarkTuple = makeQuestionMarkTuple(requiredTags.length); builder.where(Tags.TAG_ID + " IN " + questionMarkTuple, requiredTags); builder.having("COUNT(" + Qualified.SESSIONS_SESSION_ID + ") >= " + requiredTags.length); } } /** {@inheritDoc} */ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); String tagsFilter = uri.getQueryParameter(Sessions.QUERY_PARAMETER_TAG_FILTER); final int match = sUriMatcher.match(uri); // avoid the expensive string concatenation below if not loggable if (Log.isLoggable(TAG, Log.VERBOSE)) { LOGV(TAG, "uri=" + uri + " match=" + match + " proj=" + Arrays.toString(projection) + " selection=" + selection + " args=" + Arrays.toString(selectionArgs) + ")"); } switch (match) { default: { // Most cases are handled with simple SelectionBuilder final SelectionBuilder builder = buildExpandedSelection(uri, match); // If a special filter was specified, try to apply it if (!TextUtils.isEmpty(tagsFilter)) { addTagsFilter(builder, tagsFilter); } boolean distinct = !TextUtils.isEmpty( uri.getQueryParameter(ScheduleContract.QUERY_PARAMETER_DISTINCT)); Cursor cursor = builder .where(selection, selectionArgs) .query(db, distinct, projection, sortOrder, null); Context context = getContext(); if (null != context) { cursor.setNotificationUri(context.getContentResolver(), uri); } return cursor; } case SEARCH_SUGGEST: { final SelectionBuilder builder = new SelectionBuilder(); // Adjust incoming query to become SQL text match selectionArgs[0] = selectionArgs[0] + "%"; builder.table(Tables.SEARCH_SUGGEST); builder.where(selection, selectionArgs); builder.map(SearchManager.SUGGEST_COLUMN_QUERY, SearchManager.SUGGEST_COLUMN_TEXT_1); projection = new String[] { BaseColumns._ID, SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_QUERY }; final String limit = uri.getQueryParameter(SearchManager.SUGGEST_PARAMETER_LIMIT); return builder.query(db, false, projection, SearchSuggest.DEFAULT_SORT, limit); } } } /** {@inheritDoc} */ @Override public Uri insert(Uri uri, ContentValues values) { LOGV(TAG, "insert(uri=" + uri + ", values=" + values.toString() + ", account=" + getCurrentAccountName(uri, false) + ")"); final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); final int match = sUriMatcher.match(uri); boolean syncToNetwork = !ScheduleContract.hasCallerIsSyncAdapterParameter(uri); switch (match) { case BLOCKS: { db.insertOrThrow(Tables.BLOCKS, null, values); notifyChange(uri); return Blocks.buildBlockUri(values.getAsString(Blocks.BLOCK_ID)); } case TAGS: { db.insertOrThrow(Tables.TAGS, null, values); notifyChange(uri); return Tags.buildTagUri(values.getAsString(Tags.TAG_ID)); } case ROOMS: { db.insertOrThrow(Tables.ROOMS, null, values); notifyChange(uri); return Rooms.buildRoomUri(values.getAsString(Rooms.ROOM_ID)); } case SESSIONS: { db.insertOrThrow(Tables.SESSIONS, null, values); notifyChange(uri); return Sessions.buildSessionUri(values.getAsString(Sessions.SESSION_ID)); } case SESSIONS_ID_SPEAKERS: { db.insertOrThrow(Tables.SESSIONS_SPEAKERS, null, values); notifyChange(uri); return Speakers.buildSpeakerUri(values.getAsString(SessionsSpeakers.SPEAKER_ID)); } case SESSIONS_ID_TAGS: { db.insertOrThrow(Tables.SESSIONS_TAGS, null, values); notifyChange(uri); return Tags.buildTagUri(values.getAsString(Tags.TAG_ID)); } case MY_SCHEDULE: { values.put(MySchedule.MY_SCHEDULE_ACCOUNT_NAME, getCurrentAccountName(uri, false)); db.insertOrThrow(Tables.MY_SCHEDULE, null, values); notifyChange(uri); return Sessions.buildSessionUri(values.getAsString( ScheduleContract.MyScheduleColumns.SESSION_ID)); } case SPEAKERS: { db.insertOrThrow(Tables.SPEAKERS, null, values); notifyChange(uri); return Speakers.buildSpeakerUri(values.getAsString(Speakers.SPEAKER_ID)); } case ANNOUNCEMENTS: { db.insertOrThrow(Tables.ANNOUNCEMENTS, null, values); notifyChange(uri); return Announcements.buildAnnouncementUri(values .getAsString(Announcements.ANNOUNCEMENT_ID)); } case SEARCH_SUGGEST: { db.insertOrThrow(Tables.SEARCH_SUGGEST, null, values); notifyChange(uri); return SearchSuggest.CONTENT_URI; } case MAPMARKERS: { db.insertOrThrow(Tables.MAPMARKERS, null, values); notifyChange(uri); return MapMarkers.buildMarkerUri(values.getAsString(MapMarkers.MARKER_ID)); } case MAPTILES: { db.insertOrThrow(Tables.MAPTILES, null, values); notifyChange(uri); return MapTiles.buildFloorUri(values.getAsString(MapTiles.TILE_FLOOR)); } case FEEDBACK_FOR_SESSION: { db.insertOrThrow(Tables.FEEDBACK, null, values); notifyChange(uri); return Feedback.buildFeedbackUri(values.getAsString(Feedback.SESSION_ID)); } case EXPERTS: { db.insertOrThrow(Tables.EXPERTS, null, values); notifyChange(uri); return Experts.buildExpertUri(values.getAsString(Experts.EXPERT_ID)); } case HASHTAGS: { db.insertOrThrow(Tables.HASHTAGS, null, values); notifyChange(uri); return Hashtags.buildHashtagUri(values.getAsString(Hashtags.HASHTAG_NAME)); } case PEOPLE_IVE_MET: { db.insertOrThrow(Tables.PEOPLE_IVE_MET, null, values); notifyChange(uri); return ScheduleContract.PeopleIveMet.buildPersonUri(values.getAsString(PeopleIveMet.PERSON_ID)); } case VIDEOS: { db.insertOrThrow(Tables.VIDEOS, null, values); notifyChange(uri); return Videos.buildVideoUri(values.getAsString(Videos.VIDEO_ID)); } case PARTNERS: { db.insertOrThrow(Tables.PARTNERS, null, values); notifyChange(uri); return Partners.buildPartnerUri(values.getAsString(Partners.PARTNER_ID)); } default: { throw new UnsupportedOperationException("Unknown insert uri: " + uri); } } } /** {@inheritDoc} */ @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { String accountName = getCurrentAccountName(uri, false); LOGV(TAG, "update(uri=" + uri + ", values=" + values.toString() + ", account=" + accountName + ")"); final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); final int match = sUriMatcher.match(uri); if (match == SEARCH_INDEX) { // update the search index ScheduleDatabase.updateSessionSearchIndex(db); return 1; } final SelectionBuilder builder = buildSimpleSelection(uri); if (match == MY_SCHEDULE) { values.remove(MySchedule.MY_SCHEDULE_ACCOUNT_NAME); builder.where(MySchedule.MY_SCHEDULE_ACCOUNT_NAME + "=?", accountName); } int retVal = builder.where(selection, selectionArgs).update(db, values); notifyChange(uri); return retVal; } /** {@inheritDoc} */ @Override public int delete(Uri uri, String selection, String[] selectionArgs) { String accountName = getCurrentAccountName(uri, false); LOGV(TAG, "delete(uri=" + uri + ", account=" + accountName + ")"); if (uri == ScheduleContract.BASE_CONTENT_URI) { // Handle whole database deletes (e.g. when signing out) deleteDatabase(); notifyChange(uri); return 1; } final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); final SelectionBuilder builder = buildSimpleSelection(uri); final int match = sUriMatcher.match(uri); if (match == MY_SCHEDULE) { builder.where(MySchedule.MY_SCHEDULE_ACCOUNT_NAME + "=?", accountName); } int retVal = builder.where(selection, selectionArgs).delete(db); notifyChange(uri); return retVal; } private void notifyChange(Uri uri) { // We only notify changes if the caller is not the sync adapter. // The sync adapter has the responsibility of notifying changes (it can do so // more intelligently than we can -- for example, doing it only once at the end // of the sync instead of issuing thousands of notifications for each record). if (!ScheduleContract.hasCallerIsSyncAdapterParameter(uri)) { Context context = getContext(); context.getContentResolver().notifyChange(uri, null); // Widgets can't register content observers so we refresh widgets separately. context.sendBroadcast(ScheduleWidgetProvider.getRefreshBroadcastIntent(context, false)); } } /** * Apply the given set of {@link ContentProviderOperation}, executing inside * a {@link SQLiteDatabase} transaction. All changes will be rolled back if * any single one fails. */ @Override public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws OperationApplicationException { final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); db.beginTransaction(); try { final int numOperations = operations.size(); final ContentProviderResult[] results = new ContentProviderResult[numOperations]; for (int i = 0; i < numOperations; i++) { results[i] = operations.get(i).apply(this, results, i); } db.setTransactionSuccessful(); return results; } finally { db.endTransaction(); } } /** * Build a simple {@link SelectionBuilder} to match the requested * {@link Uri}. This is usually enough to support {@link #insert}, * {@link #update}, and {@link #delete} operations. */ private SelectionBuilder buildSimpleSelection(Uri uri) { final SelectionBuilder builder = new SelectionBuilder(); final int match = sUriMatcher.match(uri); switch (match) { case BLOCKS: { return builder.table(Tables.BLOCKS); } case BLOCKS_ID: { final String blockId = Blocks.getBlockId(uri); return builder.table(Tables.BLOCKS) .where(Blocks.BLOCK_ID + "=?", blockId); } case TAGS: { return builder.table(Tables.TAGS); } case TAGS_ID: { final String tagId = Tags.getTagId(uri); return builder.table(Tables.TAGS) .where(Tags.TAG_ID + "=?", tagId); } 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 SESSIONS: { return builder.table(Tables.SESSIONS); } case SESSIONS_ID: { final String sessionId = Sessions.getSessionId(uri); return builder.table(Tables.SESSIONS) .where(Sessions.SESSION_ID + "=?", sessionId); } case SESSIONS_ID_SPEAKERS: { final String sessionId = Sessions.getSessionId(uri); return builder.table(Tables.SESSIONS_SPEAKERS) .where(Sessions.SESSION_ID + "=?", sessionId); } case SESSIONS_ID_TAGS: { final String sessionId = Sessions.getSessionId(uri); return builder.table(Tables.SESSIONS_TAGS) .where(Sessions.SESSION_ID + "=?", sessionId); } case SESSIONS_MY_SCHEDULE: { final String sessionId = Sessions.getSessionId(uri); return builder.table(Tables.MY_SCHEDULE) .where(ScheduleContract.MyScheduleColumns.SESSION_ID + "=?", sessionId); } case MY_SCHEDULE: { return builder.table(Tables.MY_SCHEDULE) .where(MySchedule.MY_SCHEDULE_ACCOUNT_NAME + "=?", getCurrentAccountName(uri, false)); } case SPEAKERS: { return builder.table(Tables.SPEAKERS); } case SPEAKERS_ID: { final String speakerId = Speakers.getSpeakerId(uri); return builder.table(Tables.SPEAKERS) .where(Speakers.SPEAKER_ID + "=?", speakerId); } case ANNOUNCEMENTS: { return builder.table(Tables.ANNOUNCEMENTS); } case ANNOUNCEMENTS_ID: { final String announcementId = Announcements.getAnnouncementId(uri); return builder.table(Tables.ANNOUNCEMENTS) .where(Announcements.ANNOUNCEMENT_ID + "=?", announcementId); } case MAPMARKERS: { return builder.table(Tables.MAPMARKERS); } case MAPMARKERS_FLOOR: { final String floor = MapMarkers.getMarkerFloor(uri); return builder.table(Tables.MAPMARKERS) .where(MapMarkers.MARKER_FLOOR+ "=?", floor); } case MAPMARKERS_ID: { final String markerId = MapMarkers.getMarkerId(uri); return builder.table(Tables.MAPMARKERS) .where(MapMarkers.MARKER_ID + "=?", markerId); } case MAPTILES: { return builder.table(Tables.MAPTILES); } case MAPTILES_FLOOR: { final String floor = MapTiles.getFloorId(uri); return builder.table(Tables.MAPTILES) .where(MapTiles.TILE_FLOOR+ "=?", floor); } case SEARCH_SUGGEST: { return builder.table(Tables.SEARCH_SUGGEST); } case FEEDBACK_FOR_SESSION: { final String session_id = Feedback.getSessionId(uri); return builder.table(Tables.FEEDBACK) .where(Feedback.SESSION_ID + "=?", session_id); } case FEEDBACK_ALL: { return builder.table(Tables.FEEDBACK); } case EXPERTS: { return builder.table(Tables.EXPERTS); } case EXPERTS_ID: { String expertId = Experts.getExpertId(uri); return builder.table(Tables.EXPERTS) .where(Experts.EXPERT_ID + "= ?", expertId); } case HASHTAGS: { return builder.table(Tables.HASHTAGS); } case HASHTAGS_NAME: { final String hashtagName = Hashtags.getHashtagName(uri); return builder.table(Tables.HASHTAGS) .where(Hashtags.HASHTAG_NAME + "=?", hashtagName); } case PEOPLE_IVE_MET: { return builder.table(Tables.PEOPLE_IVE_MET); } case PEOPLE_IVE_MET_ID: { String personId = ScheduleContract.PeopleIveMet.getPersonId(uri); return builder.table(Tables.PEOPLE_IVE_MET) .where(PeopleIveMet.PERSON_ID + "=?", personId); } case VIDEOS: { return builder.table(Tables.VIDEOS); } case VIDEOS_ID: { final String videoId = Videos.getVideoId(uri); return builder.table(Tables.VIDEOS).where(Videos.VIDEO_ID + "=?", videoId); } case PARTNERS: { return builder.table(Tables.PARTNERS); } case PARTNERS_ID: { final String partnerId = Partners.getPartnerId(uri); return builder.table(Tables.PARTNERS).where(Partners.PARTNER_ID + "=?", partnerId); } default: { throw new UnsupportedOperationException("Unknown uri for " + match + ": " + uri); } } } private String getCurrentAccountName(Uri uri, boolean sanitize) { String accountName = ScheduleContract.getOverrideAccountName(uri); if (accountName == null) { accountName = AccountUtils.getActiveAccountName(getContext()); } if (sanitize) { // sanitize accountName when concatenating (http://xkcd.com/327/) accountName = (accountName != null) ? accountName.replace("'", "''") : null; } return accountName; } /** * 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) .where(Blocks.BLOCK_START + ">=?", startTime) .where(Blocks.BLOCK_START + "<=?", endTime); } case BLOCKS_ID: { final String blockId = Blocks.getBlockId(uri); return builder.table(Tables.BLOCKS) .where(Blocks.BLOCK_ID + "=?", blockId); } case TAGS: { return builder.table(Tables.TAGS); } case TAGS_ID: { final String tagId = Tags.getTagId(uri); return builder.table(Tables.TAGS) .where(Tags.TAG_ID + "=?", tagId); } 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_ROOMS) .mapToTable(Sessions._ID, Tables.SESSIONS) .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS) .where(Qualified.SESSIONS_ROOM_ID + "=?", roomId); } case SESSIONS: { // We query sessions on the joined table of sessions with rooms and tags. // Since there may be more than one tag per session, we GROUP BY session ID. // The starred sessions ("my schedule") are associated with a user, so we // use the current user to select them properly return builder.table(Tables.SESSIONS_JOIN_ROOMS_TAGS, getCurrentAccountName(uri, true)) .mapToTable(Sessions._ID, Tables.SESSIONS) .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS) .mapToTable(Sessions.SESSION_ID, Tables.SESSIONS) .map(Sessions.SESSION_IN_MY_SCHEDULE, "IFNULL(in_schedule, 0)") .groupBy(Qualified.SESSIONS_SESSION_ID); } case SESSIONS_COUNTER: { return builder.table(Tables.SESSIONS_JOIN_MYSCHEDULE, getCurrentAccountName(uri, true)) .map(Sessions.SESSION_INTERVAL_COUNT, "count(1)") .map(Sessions.SESSION_IN_MY_SCHEDULE, "IFNULL(in_schedule, 0)") .groupBy(Sessions.SESSION_START + ", " + Sessions.SESSION_END); } case SESSIONS_MY_SCHEDULE: { return builder.table(Tables.SESSIONS_JOIN_ROOMS_TAGS_FEEDBACK_MYSCHEDULE, getCurrentAccountName(uri, true)) .mapToTable(Sessions._ID, Tables.SESSIONS) .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS) .mapToTable(Sessions.SESSION_ID, Tables.SESSIONS) .map(Sessions.HAS_GIVEN_FEEDBACK, Subquery.SESSION_HAS_GIVEN_FEEDBACK) .map(Sessions.SESSION_IN_MY_SCHEDULE, "IFNULL(in_schedule, 0)") .where("( " + Sessions.SESSION_IN_MY_SCHEDULE + "=1 OR " + Sessions.SESSION_TAGS + " LIKE '%" + Config.Tags.SPECIAL_KEYNOTE + "%' )") .groupBy(Qualified.SESSIONS_SESSION_ID); } case SESSIONS_UNSCHEDULED: { final long[] interval = Sessions.getInterval(uri); return builder.table(Tables.SESSIONS_JOIN_ROOMS_TAGS_FEEDBACK_MYSCHEDULE, getCurrentAccountName(uri, true)) .mapToTable(Sessions._ID, Tables.SESSIONS) .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS) .mapToTable(Sessions.SESSION_ID, Tables.SESSIONS) .map(Sessions.SESSION_IN_MY_SCHEDULE, "IFNULL(in_schedule, 0)") .where(Sessions.SESSION_IN_MY_SCHEDULE + "=0") .where(Sessions.SESSION_START + ">=?", String.valueOf(interval[0])) .where(Sessions.SESSION_START + "<?", String.valueOf(interval[1])) .groupBy(Qualified.SESSIONS_SESSION_ID); } case SESSIONS_SEARCH: { final String query = Sessions.getSearchQuery(uri); return builder.table(Tables.SESSIONS_SEARCH_JOIN_SESSIONS_ROOMS, getCurrentAccountName(uri, true)) .map(Sessions.SEARCH_SNIPPET, Subquery.SESSIONS_SNIPPET) .mapToTable(Sessions._ID, Tables.SESSIONS) .mapToTable(Sessions.SESSION_ID, Tables.SESSIONS) .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS) .map(Sessions.SESSION_IN_MY_SCHEDULE, "IFNULL(in_schedule, 0)") .where(SessionsSearchColumns.BODY + " MATCH ?", query); } case SESSIONS_AT: { final List<String> segments = uri.getPathSegments(); final String time = segments.get(2); return builder.table(Tables.SESSIONS_JOIN_ROOMS, getCurrentAccountName(uri, true)) .mapToTable(Sessions._ID, Tables.SESSIONS) .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS) .where(Sessions.SESSION_START + "<=?", time) .where(Sessions.SESSION_END + ">=?", time); } case SESSIONS_ID: { final String sessionId = Sessions.getSessionId(uri); return builder.table(Tables.SESSIONS_JOIN_ROOMS, getCurrentAccountName(uri, true)) .mapToTable(Sessions._ID, Tables.SESSIONS) .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS) .mapToTable(Sessions.SESSION_ID, Tables.SESSIONS) .map(Sessions.SESSION_IN_MY_SCHEDULE, "IFNULL(in_schedule, 0)") .where(Qualified.SESSIONS_SESSION_ID + "=?", sessionId); } case SESSIONS_ID_SPEAKERS: { final String sessionId = Sessions.getSessionId(uri); return builder.table(Tables.SESSIONS_SPEAKERS_JOIN_SPEAKERS) .mapToTable(Speakers._ID, Tables.SPEAKERS) .mapToTable(Speakers.SPEAKER_ID, Tables.SPEAKERS) .where(Qualified.SESSIONS_SPEAKERS_SESSION_ID + "=?", sessionId); } case SESSIONS_ID_TAGS: { final String sessionId = Sessions.getSessionId(uri); return builder.table(Tables.SESSIONS_TAGS_JOIN_TAGS) .mapToTable(Tags._ID, Tables.TAGS) .mapToTable(Tags.TAG_ID, Tables.TAGS) .where(Qualified.SESSIONS_TAGS_SESSION_ID + "=?", sessionId); } case SESSIONS_ROOM_AFTER: { final String room = Sessions.getRoom(uri); final String time = Sessions.getAfter(uri); return builder.table(Tables.SESSIONS_JOIN_ROOMS, getCurrentAccountName(uri, true)) .mapToTable(Sessions._ID, Tables.SESSIONS) .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS) .where(Qualified.SESSIONS_ROOM_ID+ "=?", room) .where("("+Sessions.SESSION_START + "<= ? AND "+Sessions.SESSION_END+ " >= ?) OR ("+Sessions.SESSION_START + " >= ?)", time,time,time); } case SPEAKERS: { return builder.table(Tables.SPEAKERS); } case MY_SCHEDULE: { // force a where condition to avoid leaking schedule info to another account // Note that, since SelectionBuilder always join multiple where calls using AND, // even if malicious code specifying additional conditions on account_name won't // be able to fetch data from a different account. return builder.table(Tables.MY_SCHEDULE) .where(MySchedule.MY_SCHEDULE_ACCOUNT_NAME + "=?", getCurrentAccountName(uri, true)); } case SPEAKERS_ID: { final String speakerId = Speakers.getSpeakerId(uri); return builder.table(Tables.SPEAKERS) .where(Speakers.SPEAKER_ID + "=?", speakerId); } case SPEAKERS_ID_SESSIONS: { final String speakerId = Speakers.getSpeakerId(uri); return builder.table(Tables.SESSIONS_SPEAKERS_JOIN_SESSIONS_ROOMS) .mapToTable(Sessions._ID, Tables.SESSIONS) .mapToTable(Sessions.SESSION_ID, Tables.SESSIONS) .mapToTable(Sessions.ROOM_ID, Tables.SESSIONS) .where(Qualified.SESSIONS_SPEAKERS_SPEAKER_ID + "=?", speakerId); } case ANNOUNCEMENTS: { return builder.table(Tables.ANNOUNCEMENTS); } case ANNOUNCEMENTS_ID: { final String announcementId = Announcements.getAnnouncementId(uri); return builder.table(Tables.ANNOUNCEMENTS) .where(Announcements.ANNOUNCEMENT_ID + "=?", announcementId); } case MAPMARKERS: { return builder.table(Tables.MAPMARKERS); } case MAPMARKERS_FLOOR: { final String floor = MapMarkers.getMarkerFloor(uri); return builder.table(Tables.MAPMARKERS) .where(MapMarkers.MARKER_FLOOR + "=?", floor); } case MAPMARKERS_ID: { final String roomId = MapMarkers.getMarkerId(uri); return builder.table(Tables.MAPMARKERS) .where(MapMarkers.MARKER_ID + "=?", roomId); } case MAPTILES: { return builder.table(Tables.MAPTILES); } case MAPTILES_FLOOR: { final String floor = MapTiles.getFloorId(uri); return builder.table(Tables.MAPTILES) .where(MapTiles.TILE_FLOOR + "=?", floor); } case FEEDBACK_FOR_SESSION: { final String sessionId = Feedback.getSessionId(uri); return builder.table(Tables.FEEDBACK) .where(Feedback.SESSION_ID + "=?", sessionId); } case FEEDBACK_ALL: { return builder.table(Tables.FEEDBACK); } case EXPERTS: { return builder.table(Tables.EXPERTS); } case EXPERTS_ID: { String expertId = Experts.getExpertId(uri); return builder.table(Tables.EXPERTS) .where(Experts.EXPERT_ID + "= ?", expertId); } case HASHTAGS: { return builder.table(Tables.HASHTAGS); } case HASHTAGS_NAME: { final String hashtagName = Hashtags.getHashtagName(uri); return builder.table(Tables.HASHTAGS) .where(HashtagColumns.HASHTAG_NAME + "=?", hashtagName); } case PEOPLE_IVE_MET: { return builder.table(Tables.PEOPLE_IVE_MET); } case PEOPLE_IVE_MET_ID: { String personId = ScheduleContract.PeopleIveMet.getPersonId(uri); return builder.table(Tables.PEOPLE_IVE_MET) .where(PeopleIveMet.PERSON_ID + "=?", personId); } case VIDEOS: { return builder.table(Tables.VIDEOS); } case VIDEOS_ID: { final String videoId = Videos.getVideoId(uri); return builder.table(Tables.VIDEOS) .where(VideoColumns.VIDEO_ID + "=?", videoId); } case PARTNERS: { return builder.table(Tables.PARTNERS); } case PARTNERS_ID: { final String partnerId = Partners.getPartnerId(uri); return builder.table(Tables.PARTNERS).where(Partners.PARTNER_ID + "=?", partnerId); } default: { throw new UnsupportedOperationException("Unknown uri: " + uri); } } } @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { final int match = sUriMatcher.match(uri); switch (match) { default: { throw new UnsupportedOperationException("Unknown uri: " + uri); } } } private interface Subquery { String SESSION_HAS_GIVEN_FEEDBACK = "(SELECT COUNT(1) FROM " + Tables.FEEDBACK + " WHERE " + Qualified.FEEDBACK_SESSION_ID + "=" + Qualified.SESSIONS_SESSION_ID + ")"; String SESSIONS_SNIPPET = "snippet(" + Tables.SESSIONS_SEARCH + ",'{','}','\u2026')"; } /** * {@link ScheduleContract} fields that are fully qualified with a specific * parent {@link Tables}. Used when needed to work around SQL ambiguity. */ private interface Qualified { String SESSIONS_SESSION_ID = Tables.SESSIONS + "." + Sessions.SESSION_ID; String SESSIONS_ROOM_ID = Tables.SESSIONS + "." + Sessions.ROOM_ID; String SESSIONS_TAGS_SESSION_ID = Tables.SESSIONS_TAGS + "." + ScheduleDatabase.SessionsTags.SESSION_ID; String SESSIONS_SPEAKERS_SESSION_ID = Tables.SESSIONS_SPEAKERS + "." + SessionsSpeakers.SESSION_ID; String SESSIONS_SPEAKERS_SPEAKER_ID = Tables.SESSIONS_SPEAKERS + "." + SessionsSpeakers.SPEAKER_ID; String FEEDBACK_SESSION_ID = Tables.FEEDBACK + "." + Feedback.SESSION_ID; } }