public static String formatSessionSubtitle(long blockStart, long blockEnd,
        String roomName, Context context) {
    TimeZone.setDefault(CONFERENCE_TIME_ZONE);

    // NOTE: There is an efficient version of formatDateRange in Eclair and
    // beyond that allows you to recycle a StringBuilder.
    final CharSequence timeString = DateUtils.formatDateRange(context,
            blockStart, blockEnd, TIME_FLAGS);

    return context.getString(R.string.session_subtitle, timeString, roomName);
} 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 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 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 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 DetachableResultReceiver extends ResultReceiver {
    private static final String TAG = "DetachableResultReceiver";

    private Receiver mReceiver;

    public DetachableResultReceiver(Handler handler) {
        super(handler);
    }

    public void clearReceiver() {
        mReceiver = null;
    }

    public void setReceiver(Receiver receiver) {
        mReceiver = receiver;
    }

    public interface Receiver {
        public void onReceiveResult(int resultCode, Bundle resultData);
    }

    @Override
    protected void onReceiveResult(int resultCode, Bundle resultData) {
        if (mReceiver != null) {
            mReceiver.onReceiveResult(resultCode, resultData);
        } else {
            Log.w(TAG, "Dropping result on floor for code " + resultCode + ": "
                    + resultData.toString());
        }
    }
} public class NotifyingAsyncQueryHandler extends AsyncQueryHandler {
    private WeakReference<AsyncQueryListener> mListener;

    /**
     * Interface to listen for completed query operations.
     */
    public interface AsyncQueryListener {
        void onQueryComplete(int token, Object cookie, Cursor cursor);
    }

    public NotifyingAsyncQueryHandler(ContentResolver resolver, AsyncQueryListener listener) {
        super(resolver);
        setQueryListener(listener);
    }

    /**
     * Assign the given {@link AsyncQueryListener} to receive query events from
     * asynchronous calls. Will replace any existing listener.
     */
    public void setQueryListener(AsyncQueryListener listener) {
        mListener = new WeakReference<AsyncQueryListener>(listener);
    }

    /**
     * Clear any {@link AsyncQueryListener} set through
     * {@link #setQueryListener(AsyncQueryListener)}
     */
    public void clearQueryListener() {
        mListener = null;
    }

    /**
     * Begin an asynchronous query with the given arguments. When finished,
     * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} is
     * called if a valid {@link AsyncQueryListener} is present.
     */
    public void startQuery(Uri uri, String[] projection) {
        startQuery(-1, null, uri, projection, null, null, null);
    }

    /**
     * Begin an asynchronous query with the given arguments. When finished,
     * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} is called
     * if a valid {@link AsyncQueryListener} is present.
     *
     * @param token Unique identifier passed through to
     *            {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)}
     */
    public void startQuery(int token, Uri uri, String[] projection) {
        startQuery(token, null, uri, projection, null, null, null);
    }

    /**
     * Begin an asynchronous query with the given arguments. When finished,
     * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} is called
     * if a valid {@link AsyncQueryListener} is present.
     */
    public void startQuery(Uri uri, String[] projection, String sortOrder) {
        startQuery(-1, null, uri, projection, null, null, sortOrder);
    }

    /**
     * Begin an asynchronous query with the given arguments. When finished,
     * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} is called
     * if a valid {@link AsyncQueryListener} is present.
     */
    public void startQuery(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String orderBy) {
        startQuery(-1, null, uri, projection, selection, selectionArgs, orderBy);
    }

    /**
     * Begin an asynchronous update with the given arguments.
     */
    public void startUpdate(Uri uri, ContentValues values) {
        startUpdate(-1, null, uri, values, null, null);
    }

    public void startInsert(Uri uri, ContentValues values) {
        startInsert(-1, null, uri, values);
    }

    public void startDelete(Uri uri) {
        startDelete(-1, null, uri, null, null);
    }

    /** {@inheritDoc} */
    @Override
    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
        final AsyncQueryListener listener = mListener == null ? null : mListener.get();
        if (listener != null) {
            listener.onQueryComplete(token, cookie, cursor);
        } else if (cursor != null) {
            cursor.close();
        }
    }
} public class UIUtils {

    /**
     * Time zone to use when formatting all session times. To always use the
     * phone local time, use {@link TimeZone#getDefault()}.
     */
    public static TimeZone CONFERENCE_TIME_ZONE = TimeZone.getTimeZone("America/Denver");

    public static final long CONFERENCE_START_MILLIS = ParserUtils.parseTime(
            "2010-06-26T09:00:00.000-06:00");
    public static final long CONFERENCE_END_MILLIS = ParserUtils.parseTime(
            "2010-06-27T18:30:00.000-06:00");

    public static final Uri CONFERENCE_URL = Uri.parse("http://drupalcampcolorado.org");

    /** Flags used with {@link DateUtils#formatDateRange}. */
    private static final int TIME_FLAGS = DateUtils.FORMAT_SHOW_TIME
            | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY;

    private static final int BRIGHTNESS_THRESHOLD = 150;

    /** {@link StringBuilder} used for formatting time block. */
    private static StringBuilder sBuilder = new StringBuilder(50);
    /** {@link Formatter} used for formatting time block. */
    private static Formatter sFormatter = new Formatter(sBuilder, Locale.getDefault());

    private static StyleSpan sBoldSpan = new StyleSpan(Typeface.BOLD);

    public static void setTitleBarColor(View titleBarView, int color) {
        final ViewGroup titleBar = (ViewGroup) titleBarView;
        titleBar.setBackgroundColor(color);

        /*
         * Calculate the brightness of the titlebar color, based on the commonly known
         * brightness formula:
         *
         * http://en.wikipedia.org/wiki/HSV_color_space%23Lightness
         */
        int brColor = (30 * Color.red(color) +
                       59 * Color.green(color) +
                       11 * Color.blue(color)) / 100;
        if (brColor > BRIGHTNESS_THRESHOLD) {
            ((TextView) titleBar.findViewById(R.id.title_text)).setTextColor(
                    titleBar.getContext().getResources().getColor(R.color.title_text_alt));

            // Iterate through all children of the titlebar and if they're a LevelListDrawable,
            // set their level to 1 (alternate).
            // TODO: find a less hacky way of doing this.
            titleBar.post(new Runnable() {
                public void run() {
                    final int childCount = titleBar.getChildCount();
                    for (int i = 0; i < childCount; i++) {
                        final View child = titleBar.getChildAt(i);
                        if (child instanceof ImageButton) {
                            final ImageButton childButton = (ImageButton) child;
                            if (childButton.getDrawable() != null &&
                                childButton.getDrawable() instanceof LevelListDrawable) {
                                ((LevelListDrawable) childButton.getDrawable()).setLevel(1);
                            }
                        }
                    }
                }
            });
        }
    }

    /**
     * Invoke "home" action, returning to {@link HomeActivity}.
     */
    public static void goHome(Context context) {
        final Intent intent = new Intent(context, HomeActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        context.startActivity(intent);
    }

    /**
     * Invoke "search" action, triggering a default search.
     */
    public static void goSearch(Activity activity) {
        activity.startSearch(null, false, Bundle.EMPTY, false);
    }

    /**
     * Format and return the given {@link Blocks} and {@link Rooms} values using
     * {@link #CONFERENCE_TIME_ZONE}.
     */
    public static String formatSessionSubtitle(long blockStart, long blockEnd,
            String roomName, Context context) {
        TimeZone.setDefault(CONFERENCE_TIME_ZONE);

        // NOTE: There is an efficient version of formatDateRange in Eclair and
        // beyond that allows you to recycle a StringBuilder.
        final CharSequence timeString = DateUtils.formatDateRange(context,
                blockStart, blockEnd, TIME_FLAGS);

        return context.getString(R.string.session_subtitle, timeString, roomName);
    }

    /**
     * Populate the given {@link TextView} with the requested text, formatting
     * through {@link Html#fromHtml(String)} when applicable. Also sets
     * {@link TextView#setMovementMethod} so inline links are handled.
     */
    public static void setTextMaybeHtml(TextView view, String text) {
        if (text.contains("<") && text.contains(">")) {
            view.setText(Html.fromHtml(text));
            view.setMovementMethod(LinkMovementMethod.getInstance());
        } else {
            view.setText(text);
        }
    }

    public static void setSessionTitleColor(long blockStart, long blockEnd, TextView title,
            TextView subtitle) {
        long currentTimeMillis = System.currentTimeMillis();
        int colorId = android.R.color.primary_text_light;
        int subColorId = android.R.color.secondary_text_light;

        if (currentTimeMillis > blockEnd &&
                currentTimeMillis < CONFERENCE_END_MILLIS) {
            colorId = subColorId = R.color.session_foreground_past;
        }

        final Resources res = title.getResources();
        title.setTextColor(res.getColor(colorId));
        subtitle.setTextColor(res.getColor(subColorId));
    }

    /**
     * Given a snippet string with matching segments surrounded by curly
     * braces, turn those areas into bold spans, removing the curly braces.
     */
    public static Spannable buildStyledSnippet(String snippet) {
        final SpannableStringBuilder builder = new SpannableStringBuilder(snippet);

        // Walk through string, inserting bold snippet spans
        int startIndex = -1, endIndex = -1, delta = 0;
        while ((startIndex = snippet.indexOf('{', endIndex)) != -1) {
            endIndex = snippet.indexOf('}', startIndex);

            // Remove braces from both sides
            builder.delete(startIndex - delta, startIndex - delta + 1);
            builder.delete(endIndex - delta - 1, endIndex - delta);

            // Insert bold style
            builder.setSpan(sBoldSpan, startIndex - delta, endIndex - delta - 1,
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

            delta += 2;
        }

        return builder;
    }
} public interface AsyncQueryListener {
    void onQueryComplete(int token, Object cookie, Cursor cursor);
}
import static com.underhilllabs.dccsched.util.UIUtils.formatSessionSubtitle;
import com.underhilllabs.dccsched.R;
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.Sessions;
import com.underhilllabs.dccsched.provider.ScheduleContract.Tracks;
import com.underhilllabs.dccsched.service.SyncService;
import com.underhilllabs.dccsched.util.DetachableResultReceiver;
import com.underhilllabs.dccsched.util.NotifyingAsyncQueryHandler;
import com.underhilllabs.dccsched.util.UIUtils;
import com.underhilllabs.dccsched.util.NotifyingAsyncQueryHandler.AsyncQueryListener;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
import android.text.format.DateUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.TextView;
import android.widget.Toast;
/*
 * Copyright 2010 Google Inc.
 *
 * 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.underhilllabs.dccsched.ui;




/**
 * Front-door {@link Activity} that displays high-level features the schedule
 * application offers to users.
 */
public class HomeActivity extends Activity implements AsyncQueryListener,
        DetachableResultReceiver.Receiver {
    private static final String TAG = "HomeActivity";

    /** State held between configuration changes. */
    private State mState;

    private Handler mMessageHandler = new Handler();
    private NotifyingAsyncQueryHandler mQueryHandler;

    private TextView mCountdownTextView;
    private View mNowPlayingLoadingView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);

        mNowPlayingLoadingView = findViewById(R.id.now_playing_loading);

        mState = (State) getLastNonConfigurationInstance();
        final boolean previousState = mState != null;

        if (previousState) {
            // Start listening for SyncService updates again
            mState.mReceiver.setReceiver(this);
            updateRefreshStatus();
            reloadNowPlaying(true);

        } else {
            mState = new State();
            mState.mReceiver.setReceiver(this);
            onRefreshClick(null);
        }

        // Set up handler for now playing session query.
        mQueryHandler = new NotifyingAsyncQueryHandler(getContentResolver(), this);
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        // Clear any strong references to this Activity, we'll reattach to
        // handle events on the other side.
        mState.mReceiver.clearReceiver();
        return mState;
    }

    @Override
    protected void onResume() {
        super.onResume();

        // If 'tap here to enable Wifi' was shown, check if the user has enabled Wifi
        if (mState.mNowPlayingUri != null || mState.mNowPlayingGotoWifi) {
            reloadNowPlaying(false);
        } else if (mState.mNoResults) {
            showNowPlayingNoResults();
        }
    }

    /** Handle "refresh" title-bar action. */
    public void onRefreshClick(View v) {
        // trigger off background sync
        final Intent intent = new Intent(Intent.ACTION_SYNC, null, this, SyncService.class);
        intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, mState.mReceiver);
        startService(intent);

        reloadNowPlaying(true);
    }

    /** Handle "search" title-bar action. */
    public void onSearchClick(View v) {
        UIUtils.goSearch(this);
    }

    /** Handle "schedule" action. */
    public void onScheduleClick(View v) {
        // Launch overall conference schedule
        startActivity(new Intent(this, ScheduleActivity.class));
    }

    /** Handle "map" action. */
    public void onMapClick(View v) {
        // Launch map of conference venue
        startActivity(new Intent(this, MapActivity.class));
    }

    /** Handle "sessions" action. */
    public void onSessionsClick(View v) {
        // Launch sessions clustered by track
        final Intent intent = new Intent(Intent.ACTION_VIEW, Tracks.CONTENT_URI);
        intent.putExtra(Intent.EXTRA_TITLE, getString(R.string.title_session_tracks));
        intent.putExtra(TrackDetailActivity.EXTRA_FOCUS_TAG, TrackDetailActivity.TAG_SESSIONS);
        startActivity(intent);
    }

    /** Handle "starred" action. */
    public void onStarredClick(View v) {
        // Launch list of sessions user has starred
        startActivity(new Intent(this, StarredActivity.class));
    }

    /** Handle "sandbox" or "vendors" action. */
    public void onVendorsClick(View v) {
        // Launch list of vendors at conference
        final Intent intent = new Intent(Intent.ACTION_VIEW, Tracks.CONTENT_URI);
        intent.putExtra(Intent.EXTRA_TITLE, getString(R.string.title_vendor_tracks));
        intent.putExtra(TrackDetailActivity.EXTRA_FOCUS_TAG, TrackDetailActivity.TAG_VENDORS);
        startActivity(intent);
    }

    /** Handle "my notes" action. */
    public void onNotesClick(View v) {
        // Launch list of notes user has taken
        startActivity(new Intent(Intent.ACTION_VIEW, Notes.CONTENT_URI));
    }

    /** Handle "now playing" action. */
    public void onNowPlayingClick(View v) {
        // Shortcut launch directly to session the user is currently attending
        // (or to enable wifi if wifi is off)
        if (mState.mNowPlayingGotoWifi) {
            Intent wifiSettingsIntent = new Intent();
            wifiSettingsIntent.setAction(Settings.ACTION_WIFI_SETTINGS);
            startActivity(wifiSettingsIntent);
        } else if (mState.mNowPlayingUri != null) {
            startActivity(new Intent(Intent.ACTION_VIEW, mState.mNowPlayingUri));
        }
    }

    /** Handle "now playing > more" action. */
    public void onNowPlayingMoreClick(View v) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(Sessions.buildSessionsAtDirUri(System.currentTimeMillis()));