OSDN Git Service

Code drop
authorJohn Reck <jreck@google.com>
Mon, 28 Jan 2013 21:35:56 +0000 (13:35 -0800)
committerJohn Reck <jreck@google.com>
Mon, 28 Jan 2013 21:38:55 +0000 (13:38 -0800)
Change-Id: Ibee7c3a1aed61dbe9d3d62ad7f3d3653994c8ef8

AndroidManifest.xml
res/xml/canvas_info.xml [new file with mode: 0644]
src/com/android/gallery3d/provider/CanvasProvider.java [new file with mode: 0644]
src/com/google/android/canvas/data/Cluster.java [new file with mode: 0644]
src/com/google/android/canvas/data/util/UriUtils.java [new file with mode: 0644]
src/com/google/android/canvas/provider/CanvasContract.java [new file with mode: 0644]

index 7034aa9..360128c 100644 (file)
                 <data android:mimeType="vnd.android.cursor.dir/image" />
                 <data android:mimeType="vnd.android.cursor.dir/video" />
             </intent-filter>
+            <meta-data
+                android:name="com.google.android.canvas.data.launcher_info"
+                android:resource="@xml/canvas_info" />
         </activity>
 
         <!-- we add this activity-alias for shortcut backward compatibility -->
                 android:theme="@style/Theme.ProxyLauncher">
         </activity>
         <service android:name="com.android.gallery3d.app.BatchService" />
+        <!-- canvas -->
+        <provider
+            android:name="com.android.gallery3d.provider.CanvasProvider"
+            android:authorities="com.android.gallery3d.provider.CanvasProvider"
+            android:exported="true"
+            android:label="@string/app_name"
+            android:permission="android.permission.ACCESS_APP_BROWSE_DATA" />
     </application>
 </manifest>
diff --git a/res/xml/canvas_info.xml b/res/xml/canvas_info.xml
new file mode 100644 (file)
index 0000000..225da40
--- /dev/null
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Launcher data. -->
+<launcher version="1">
+    <rootUri>content://com.android.gallery3d.provider.CanvasProvider/launcher</rootUri>
+</launcher>
diff --git a/src/com/android/gallery3d/provider/CanvasProvider.java b/src/com/android/gallery3d/provider/CanvasProvider.java
new file mode 100644 (file)
index 0000000..86c72fb
--- /dev/null
@@ -0,0 +1,660 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.android.gallery3d.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.provider.BaseColumns;
+import android.util.Log;
+
+import com.android.gallery3d.app.GalleryApp;
+import com.android.gallery3d.data.ContentListener;
+import com.android.gallery3d.data.DataManager;
+import com.android.gallery3d.data.MediaItem;
+import com.android.gallery3d.data.MediaSet;
+import com.android.gallery3d.data.MediaSet.SyncListener;
+import com.android.gallery3d.util.Future;
+import com.android.gallery3d.util.ThreadPool.CancelListener;
+import com.android.gallery3d.util.ThreadPool.Job;
+import com.android.gallery3d.util.ThreadPool.JobContext;
+import com.google.android.canvas.data.Cluster;
+import com.google.android.canvas.provider.CanvasContract;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class CanvasProvider extends ContentProvider {
+
+    private static final String TAG = "GalleryCanvasProvider";
+
+    private static final String AUTHORITY = "com.android.gallery3d.provider.CanvasProvider";
+
+    public static Uri NOTIFY_CHANGED_URI = Uri.parse("content://" + AUTHORITY);
+
+    private static final String PATH_IMAGE = "image";
+
+    private static final String PATH_LAUNCHER = "launcher";
+    private static final String PATH_LAUNCHER_ITEM = PATH_LAUNCHER + "/"
+            + CanvasContract.PATH_LAUNCHER_ITEM;
+    private static final String PATH_BROWSE = "browse";
+    private static final String PATH_BROWSE_HEADERS = PATH_BROWSE + "/"
+            + CanvasContract.PATH_BROWSE_HEADERS;
+
+    private static final int LAUNCHER = 1;
+    private static final int LAUNCHER_ITEMS = 2;
+    private static final int LAUNCHER_ITEM_ID = 3;
+    private static final int BROWSE_HEADERS = 4;
+    private static final int BROWSE = 5;
+    private static final int IMAGE = 6;
+
+    private static final Uri BROWSER_ROOT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH_BROWSE);
+
+    private static final UriMatcher sUriMatcher = new UriMatcher(
+            UriMatcher.NO_MATCH);
+
+    static {
+        sUriMatcher.addURI(AUTHORITY, PATH_LAUNCHER, LAUNCHER);
+        sUriMatcher.addURI(AUTHORITY, PATH_LAUNCHER_ITEM, LAUNCHER_ITEMS);
+        sUriMatcher.addURI(AUTHORITY, PATH_LAUNCHER_ITEM + "/#",
+                LAUNCHER_ITEM_ID);
+        sUriMatcher.addURI(AUTHORITY, PATH_BROWSE_HEADERS, BROWSE_HEADERS);
+        sUriMatcher.addURI(AUTHORITY, PATH_BROWSE + "/#", BROWSE);
+        sUriMatcher.addURI(AUTHORITY, PATH_IMAGE + "/*", IMAGE);
+    }
+
+    private static final HashMap<String, Integer> LAUNCHER_COLUMN_CASES = new HashMap<String, Integer>();
+    private static final String[] LAUNCHER_PROJECTION_ALL;
+
+    private static final int LAUNCHER_CASE_ID = 0;
+    private static final int LAUNCHER_CASE_COUNT = 1;
+    private static final int LAUNCHER_CASE_NAME = 2;
+    private static final int LAUNCHER_CASE_IMPORTANCE = 3;
+    private static final int LAUNCHER_CASE_DISPLAY_NAME = 4;
+    private static final int LAUNCHER_CASE_VISIBLE_COUNT = 5;
+    private static final int LAUNCHER_CASE_CROP_ALLOWED = 6;
+    private static final int LAUNCHER_CASE_CACHE_TIME = 7;
+    private static final int LAUNCHER_CASE_INTENT_URI = 8;
+
+    static {
+        LAUNCHER_COLUMN_CASES
+                .put(CanvasContract.Launcher._ID, LAUNCHER_CASE_ID);
+        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher._COUNT,
+                LAUNCHER_CASE_COUNT);
+        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.NAME,
+                LAUNCHER_CASE_NAME);
+        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.IMPORTANCE,
+                LAUNCHER_CASE_IMPORTANCE);
+        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.DISPLAY_NAME,
+                LAUNCHER_CASE_DISPLAY_NAME);
+        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.VISIBLE_COUNT,
+                LAUNCHER_CASE_VISIBLE_COUNT);
+        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.IMAGE_CROP_ALLOWED,
+                LAUNCHER_CASE_CROP_ALLOWED);
+        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.CACHE_TIME_MS,
+                LAUNCHER_CASE_CACHE_TIME);
+        LAUNCHER_COLUMN_CASES.put(CanvasContract.Launcher.INTENT_URI,
+                LAUNCHER_CASE_INTENT_URI);
+
+        LAUNCHER_PROJECTION_ALL = LAUNCHER_COLUMN_CASES.keySet().toArray(
+                new String[] {});
+    }
+
+    private static final HashMap<String, Integer> CLUSTER_COLUMN_CASES = new HashMap<String, Integer>();
+    private static final String[] CLUSTER_PROJECTION_ALL;
+
+    private static final int CLUSTER_CASE_ID = 0;
+    private static final int CLUSTER_CASE_COUNT = 1;
+    private static final int CLUSTER_CASE_PARENT_ID = 2;
+    private static final int CLUSTER_CASE_IMAGE_URI = 3;
+
+    static {
+        CLUSTER_COLUMN_CASES.put(CanvasContract.LauncherItem._ID,
+                CLUSTER_CASE_ID);
+        CLUSTER_COLUMN_CASES.put(CanvasContract.LauncherItem._COUNT,
+                CLUSTER_CASE_COUNT);
+        CLUSTER_COLUMN_CASES.put(CanvasContract.LauncherItem.PARENT_ID,
+                CLUSTER_CASE_PARENT_ID);
+        CLUSTER_COLUMN_CASES.put(CanvasContract.LauncherItem.IMAGE_URI,
+                CLUSTER_CASE_IMAGE_URI);
+
+        CLUSTER_PROJECTION_ALL = CLUSTER_COLUMN_CASES.keySet().toArray(
+                new String[] {});
+    }
+
+    private static final HashMap<String, Integer> BROWSE_HEADER_COLUMN_CASES = new HashMap<String, Integer>();
+    private static final String[] BROWSE_HEADER_PROJECTION_ALL;
+
+    private static final int BROWSE_HEADER_CASE_ID = 0;
+    private static final int BROWSE_HEADER_CASE_COUNT = 1;
+    private static final int BROWSE_HEADER_CASE_NAME = 2;
+    private static final int BROWSE_HEADER_CASE_DISPLAY_NAME = 3;
+    private static final int BROWSE_HEADER_CASE_ICON_URI = 4;
+    private static final int BROWSE_HEADER_CASE_BADGE_URI = 5;
+    private static final int BROWSE_HEADER_CASE_COLOR_HINT = 6;
+    private static final int BROWSE_HEADER_CASE_TEXT_COLOR_HINT = 7;
+    private static final int BROWSE_HEADER_CASE_BG_IMAGE_URI = 8;
+    private static final int BROWSE_HEADER_CASE_EXPAND_GROUP = 9;
+    private static final int BROWSE_HEADER_CASE_WRAP = 10;
+    private static final int BROWSE_HEADER_CASE_DEFAULT_ITEM_WIDTH = 11;
+    private static final int BROWSE_HEADER_CASE_DEFAULT_ITEM_HEIGHT = 12;
+
+    static {
+        BROWSE_HEADER_COLUMN_CASES.put(CanvasContract.BrowseHeaders._ID,
+                BROWSE_HEADER_CASE_ID);
+        BROWSE_HEADER_COLUMN_CASES.put(CanvasContract.BrowseHeaders._COUNT,
+                BROWSE_HEADER_CASE_COUNT);
+        BROWSE_HEADER_COLUMN_CASES.put(CanvasContract.BrowseHeaders.NAME,
+                BROWSE_HEADER_CASE_NAME);
+        BROWSE_HEADER_COLUMN_CASES.put(
+                CanvasContract.BrowseHeaders.DISPLAY_NAME,
+                BROWSE_HEADER_CASE_DISPLAY_NAME);
+        BROWSE_HEADER_COLUMN_CASES.put(CanvasContract.BrowseHeaders.ICON_URI,
+                BROWSE_HEADER_CASE_ICON_URI);
+        BROWSE_HEADER_COLUMN_CASES.put(CanvasContract.BrowseHeaders.BADGE_URI,
+                BROWSE_HEADER_CASE_BADGE_URI);
+        BROWSE_HEADER_COLUMN_CASES.put(CanvasContract.BrowseHeaders.COLOR_HINT,
+                BROWSE_HEADER_CASE_COLOR_HINT);
+        BROWSE_HEADER_COLUMN_CASES.put(
+                CanvasContract.BrowseHeaders.TEXT_COLOR_HINT,
+                BROWSE_HEADER_CASE_TEXT_COLOR_HINT);
+        BROWSE_HEADER_COLUMN_CASES.put(
+                CanvasContract.BrowseHeaders.BG_IMAGE_URI,
+                BROWSE_HEADER_CASE_BG_IMAGE_URI);
+        BROWSE_HEADER_COLUMN_CASES.put(
+                CanvasContract.BrowseHeaders.EXPAND_GROUP,
+                BROWSE_HEADER_CASE_EXPAND_GROUP);
+        BROWSE_HEADER_COLUMN_CASES.put(CanvasContract.BrowseHeaders.WRAP_ITEMS,
+                BROWSE_HEADER_CASE_WRAP);
+        BROWSE_HEADER_COLUMN_CASES.put(
+                CanvasContract.BrowseHeaders.DEFAULT_ITEM_WIDTH,
+                BROWSE_HEADER_CASE_DEFAULT_ITEM_WIDTH);
+        BROWSE_HEADER_COLUMN_CASES.put(
+                CanvasContract.BrowseHeaders.DEFAULT_ITEM_HEIGHT,
+                BROWSE_HEADER_CASE_DEFAULT_ITEM_HEIGHT);
+
+        BROWSE_HEADER_PROJECTION_ALL = BROWSE_HEADER_COLUMN_CASES.keySet()
+                .toArray(new String[] {});
+    }
+
+    private static final HashMap<String, Integer> BROWSE_COLUMN_CASES = new HashMap<String, Integer>();
+    private static final String[] BROWSE_PROJECTION_ALL;
+
+    private static final int BROWSE_CASE_ID = 0;
+    private static final int BROWSE_CASE_COUNT = 1;
+    private static final int BROWSE_CASE_PARENT_ID = 2;
+    private static final int BROWSE_CASE_DISPLAY_NAME = 3;
+    private static final int BROWSE_CASE_DISPLAY_DESCRIPTION = 4;
+    private static final int BROWSE_CASE_IMAGE_URI = 5;
+    private static final int BROWSE_CASE_WIDTH = 6;
+    private static final int BROWSE_CASE_HEIGHT = 7;
+    private static final int BROWSE_CASE_INTENT_URI = 8;
+
+    static {
+        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems._ID, BROWSE_CASE_ID);
+        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems._COUNT,
+                BROWSE_CASE_COUNT);
+        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.PARENT_ID,
+                BROWSE_CASE_PARENT_ID);
+        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.DISPLAY_NAME,
+                BROWSE_CASE_DISPLAY_NAME);
+        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.DISPLAY_DESCRIPTION,
+                BROWSE_CASE_DISPLAY_DESCRIPTION);
+        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.IMAGE_URI,
+                BROWSE_CASE_IMAGE_URI);
+        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.WIDTH,
+                BROWSE_CASE_WIDTH);
+        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.HEIGHT,
+                BROWSE_CASE_HEIGHT);
+        BROWSE_COLUMN_CASES.put(CanvasContract.BrowseItems.INTENT_URI,
+                BROWSE_CASE_INTENT_URI);
+
+        BROWSE_PROJECTION_ALL = BROWSE_COLUMN_CASES.keySet().toArray(
+                new String[] {});
+    }
+
+    // The max clusters that we'll return for a single launcher
+    private static final int MAX_CLUSTER_SIZE = 3;
+    // The max amount of items we'll return for a cluster
+    private static final int MAX_CLUSTER_ITEM_SIZE = 10;
+    private static final Integer CACHE_TIME_MS = 1*1000;
+
+    private DataManager mDataManager;
+    private MediaSet mRootSet;
+    private ArrayList<Cluster> mClusters = new ArrayList<Cluster>(MAX_CLUSTER_SIZE);
+
+    @Override
+    public boolean onCreate() {
+        GalleryApp app = (GalleryApp) getContext().getApplicationContext();
+        mDataManager = app.getDataManager();
+        return true;
+    }
+
+    private final static SyncListener sNullSyncListener = new SyncListener() {
+        @Override
+        public void onSyncDone(MediaSet mediaSet, int resultCode) {
+        }
+    };
+
+    private final ContentListener mChangedListener = new ContentListener() {
+        @Override
+        public void onContentDirty() {
+            getContext().getContentResolver().notifyChange(
+                    NOTIFY_CHANGED_URI, null, false);
+        }
+    };
+
+    private MediaSet loadRootMediaSet() {
+        if (mRootSet == null) {
+            String path = mDataManager.getTopSetPath(DataManager.INCLUDE_ALL);
+            mRootSet = mDataManager.getMediaSet(path);
+        }
+        loadMediaSet(mRootSet);
+        return mRootSet;
+    }
+
+    private void loadMediaSet(MediaSet set) {
+        try {
+            Future<Integer> future = set.requestSync(sNullSyncListener);
+            synchronized (future) {
+                if (!future.isDone()) {
+                    future.wait(100);
+                }
+            }
+        } catch (InterruptedException e) {
+            Log.d(TAG, "timed out waiting for sync");
+        }
+        set.addContentListener(mChangedListener);
+        set.loadIfDirty();
+    }
+
+    private void loadClustersIfEmpty() {
+        if (mClusters.size() > 0) {
+            return;
+        }
+
+        MediaSet root = loadRootMediaSet();
+        int count = root.getSubMediaSetCount();
+        for (int i = 0; i < count && mClusters.size() < MAX_CLUSTER_SIZE; i++) {
+            MediaSet set = root.getSubMediaSet(i);
+            loadMediaSet(set);
+            Log.d(TAG, "Building set: " + set.getName());
+            Cluster.Builder bob = new Cluster.Builder();
+            bob.id(i);
+            bob.displayName(set.getName());
+            Intent intent = CanvasContract.getBrowseIntent(BROWSER_ROOT_URI, i);
+            bob.intent(intent);
+            bob.imageCropAllowed(true);
+            bob.cacheTimeMs(CACHE_TIME_MS);
+            int itemCount = Math.min(set.getMediaItemCount(), MAX_CLUSTER_ITEM_SIZE);
+            List<MediaItem> items = set.getMediaItem(0, itemCount);
+            if (itemCount != items.size()) {
+                Log.d(TAG, "Size mismatch, expected " + itemCount + ", got " + items.size());
+            }
+            // This is done because not all items may have been synced yet
+            itemCount = items.size();
+            if (itemCount <= 0) {
+                Log.d(TAG, "Skipping, no items...");
+            }
+            bob.visibleCount(itemCount);
+            for (MediaItem item : items) {
+                bob.addItem(createImageUri(item));
+            }
+            mClusters.add(bob.build());
+        }
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        long identity = Binder.clearCallingIdentity();
+        try {
+            MatrixCursor c;
+            int match = sUriMatcher.match(uri);
+            Log.d(TAG, "query: " + uri.toString() + ", match = " + match);
+            switch (match) {
+            case LAUNCHER:
+                if (projection == null) {
+                    projection = LAUNCHER_PROJECTION_ALL;
+                }
+                c = new MatrixCursor(projection);
+                buildClusters(projection, c);
+                break;
+            case LAUNCHER_ITEMS:
+                if (projection == null) {
+                    projection = CLUSTER_PROJECTION_ALL;
+                }
+                c = new MatrixCursor(projection);
+                buildMultiCluster(projection, c, uri);
+                break;
+            case LAUNCHER_ITEM_ID:
+                if (projection == null) {
+                    projection = CLUSTER_PROJECTION_ALL;
+                }
+                c = new MatrixCursor(projection);
+                buildSingleCluster(projection, c, uri);
+                break;
+            case BROWSE_HEADERS:
+                if (projection == null) {
+                    projection = BROWSE_HEADER_PROJECTION_ALL;
+                }
+                c = new MatrixCursor(projection);
+                buildBrowseHeaders(projection, c);
+                break;
+            case BROWSE:
+                if (projection == null) {
+                    projection = BROWSE_PROJECTION_ALL;
+                }
+                c = new MatrixCursor(projection);
+                buildBrowseRow(projection, c, uri);
+                break;
+            default:
+                c = new MatrixCursor(new String[] {BaseColumns._ID});
+                break;
+            }
+            return c;
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private static final JobContext sJobStub = new JobContext() {
+        @Override
+        public boolean isCancelled() {
+            return false;
+        }
+
+        @Override
+        public void setCancelListener(CancelListener listener) {
+        }
+
+        @Override
+        public boolean setMode(int mode) {
+            return true;
+        }
+    };
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode) {
+        long identity = Binder.clearCallingIdentity();
+        try {
+            String path = uri.getQueryParameter("path");
+            MediaItem item = (MediaItem) mDataManager.getMediaObject(path);
+            Job<Bitmap> job = item.requestImage(MediaItem.TYPE_MICROTHUMBNAIL);
+            final Bitmap bitmap = job.run(sJobStub);
+            final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
+            AsyncTask<Object, Object, Object> task = new AsyncTask<Object, Object, Object>() {
+                @Override
+                protected Object doInBackground(Object... params) {
+                    OutputStream stream = new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]);
+                    bitmap.compress(CompressFormat.PNG, 100, stream);
+                    try {
+                        fds[1].close();
+                    } catch (IOException e) {
+                        Log.w(TAG, "Failure closing pipe", e);
+                    }
+                    return null;
+                }
+            };
+            task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Object[])null);
+
+            return fds[0];
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private Uri createImageUri(MediaItem item) {
+        // TODO: Make a database to track URIs we've actually returned
+        // for which to proxy to avoid things with
+        // android.permission.ACCESS_APP_BROWSE_DATA being able to make
+        // any request it wants on our behalf.
+        return new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(AUTHORITY)
+                .path(PATH_IMAGE)
+                .appendQueryParameter("path", item.getPath().toString())
+                .build();
+    }
+
+    private void buildClusters(String[] projection, MatrixCursor c) {
+        mClusters.clear();
+        loadClustersIfEmpty();
+
+        int clusterCount = mClusters.size();
+        for (Cluster cluster : mClusters) {
+
+            Object[] row = new Object[projection.length];
+            long id = cluster.getId();
+            for (int j = 0; j < projection.length; j++) {
+                if (!LAUNCHER_COLUMN_CASES.containsKey(projection[j])) {
+                    continue;
+                }
+                int column = LAUNCHER_COLUMN_CASES.get(projection[j]);
+                Object obj = null;
+                switch (column) {
+                    case LAUNCHER_CASE_ID:
+                        obj = id;
+                        break;
+                    case LAUNCHER_CASE_COUNT:
+                        obj = clusterCount;
+                        break;
+                    case LAUNCHER_CASE_NAME:
+                        obj = cluster.getName();
+                        break;
+                    case LAUNCHER_CASE_IMPORTANCE:
+                        obj = cluster.getImportance();
+                        break;
+                    case LAUNCHER_CASE_DISPLAY_NAME:
+                        obj = cluster.getDisplayName();
+                        break;
+                    case LAUNCHER_CASE_VISIBLE_COUNT:
+                        obj = cluster.getVisibleCount();
+                        break;
+                    case LAUNCHER_CASE_CACHE_TIME:
+                        obj = cluster.getCacheTimeMs();
+                        break;
+                    case LAUNCHER_CASE_INTENT_URI:
+                        obj = cluster.getIntent().toUri(Intent.URI_INTENT_SCHEME);
+                        break;
+                    case LAUNCHER_CASE_CROP_ALLOWED:
+                        obj = cluster.isImageCropAllowed();
+                        break;
+                }
+                row[j] = obj;
+            }
+            c.addRow(row);
+        }
+    }
+
+    private void buildMultiCluster(String[] projection, MatrixCursor c, Uri uri) {
+        for (int index = 0; index < mClusters.size(); ++index) {
+            buildSingleCluster(projection, c,
+                    uri.buildUpon().appendPath(String.valueOf(index)).build());
+        }
+    }
+
+    private void buildSingleCluster(String[] projection, MatrixCursor c, Uri uri) {
+        loadClustersIfEmpty();
+
+        int parentId = Integer.parseInt(uri.getLastPathSegment());
+
+        Cluster cluster = mClusters.get(parentId);
+        int numItems = Math.min(cluster.getItemCount(), MAX_CLUSTER_ITEM_SIZE);
+        for (int i = 0; i < numItems; i++) {
+            Cluster.ClusterItem item = cluster.getItem(i);
+            Object[] row = new Object[projection.length];
+
+            for (int j = 0; j < projection.length; j++) {
+                if (!CLUSTER_COLUMN_CASES.containsKey(projection[j])) {
+                    continue;
+                }
+                int column = CLUSTER_COLUMN_CASES.get(projection[j]);
+                switch (column) {
+                    case CLUSTER_CASE_ID:
+                        row[j] = i;
+                        break;
+                    case CLUSTER_CASE_COUNT:
+                        row[j] = numItems;
+                        break;
+                    case CLUSTER_CASE_PARENT_ID:
+                        row[j] = parentId;
+                        break;
+                    case CLUSTER_CASE_IMAGE_URI:
+                        row[j] = item.getImageUri();
+                        break;
+                }
+            }
+            c.addRow(row);
+        }
+    }
+
+    private void buildBrowseHeaders(String[] projection, MatrixCursor c) {
+        // TODO: All images
+        MediaSet root = loadRootMediaSet();
+        int itemCount = root.getSubMediaSetCount();
+        for (int i = 0; i < itemCount; i++) {
+            Object[] header = new Object[projection.length];
+            MediaSet item = root.getSubMediaSet(i);
+            for (int j = 0; j < projection.length; j++) {
+                if (!BROWSE_HEADER_COLUMN_CASES.containsKey(projection[j])) {
+                    continue;
+                }
+                int column = BROWSE_HEADER_COLUMN_CASES.get(projection[j]);
+                Object obj = null;
+                switch (column) {
+                    case BROWSE_HEADER_CASE_ID:
+                        obj = i;
+                        break;
+                    case BROWSE_HEADER_CASE_COUNT:
+                        obj = itemCount;
+                        break;
+                    case BROWSE_HEADER_CASE_NAME:
+                    case BROWSE_HEADER_CASE_DISPLAY_NAME:
+                        obj = item.getName();
+                        break;
+                    case BROWSE_HEADER_CASE_ICON_URI:
+                        break;
+                    case BROWSE_HEADER_CASE_BADGE_URI:
+                        break;
+                    case BROWSE_HEADER_CASE_COLOR_HINT:
+                        break;
+                    case BROWSE_HEADER_CASE_TEXT_COLOR_HINT:
+                        break;
+                    case BROWSE_HEADER_CASE_BG_IMAGE_URI:
+                        break;
+                    case BROWSE_HEADER_CASE_EXPAND_GROUP:
+                        obj = 0;
+                        break;
+                    case BROWSE_HEADER_CASE_WRAP:
+                        obj = i % 2;
+                        break;
+                    case BROWSE_HEADER_CASE_DEFAULT_ITEM_WIDTH:
+                    case BROWSE_HEADER_CASE_DEFAULT_ITEM_HEIGHT:
+                        obj = MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL);
+                        break;
+                }
+                header[j] = obj;
+            }
+            c.addRow(header);
+        }
+    }
+
+    private void buildBrowseRow(String[] projection, MatrixCursor c, Uri uri) {
+        int row = Integer.parseInt(uri.getLastPathSegment());
+        MediaSet album = loadRootMediaSet().getSubMediaSet(row);
+        loadMediaSet(album);
+        int itemCount = album.getMediaItemCount();
+        ArrayList<MediaItem> items = album.getMediaItem(0, itemCount);
+        itemCount = items.size();
+        for (int i = 0; i < itemCount; i++) {
+            Object[] header = new Object[projection.length];
+            MediaItem item = items.get(i);
+            for (int j = 0; j < projection.length; j++) {
+                if (!BROWSE_COLUMN_CASES.containsKey(projection[j])) {
+                    continue;
+                }
+                int column = BROWSE_COLUMN_CASES.get(projection[j]);
+                Object obj = null;
+                switch (column) {
+                    case BROWSE_CASE_ID:
+                        obj = i;
+                        break;
+                    case BROWSE_CASE_COUNT:
+                        obj = itemCount;
+                        break;
+                    case BROWSE_CASE_DISPLAY_NAME:
+                        obj = item.getName();
+                        break;
+                    case BROWSE_CASE_DISPLAY_DESCRIPTION:
+                        obj = item.getFilePath();
+                        break;
+                    case BROWSE_CASE_IMAGE_URI:
+                        obj = createImageUri(item);
+                        break;
+                    case BROWSE_CASE_WIDTH:
+                    case BROWSE_CASE_HEIGHT:
+                        obj = MediaItem.getTargetSize(MediaItem.TYPE_MICROTHUMBNAIL);
+                        break;
+                    case BROWSE_CASE_INTENT_URI:
+                        Intent intent = new Intent(Intent.ACTION_VIEW, item.getContentUri());
+                        obj = intent.toUri(Intent.URI_INTENT_SCHEME);
+                        break;
+                }
+                header[j] = obj;
+            }
+            c.addRow(header);
+        }
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException("Insert not supported");
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException("Delete not supported");
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        throw new UnsupportedOperationException("Update not supported");
+    }
+
+}
diff --git a/src/com/google/android/canvas/data/Cluster.java b/src/com/google/android/canvas/data/Cluster.java
new file mode 100644 (file)
index 0000000..ab6aaed
--- /dev/null
@@ -0,0 +1,195 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.android.canvas.data;
+
+import android.content.Intent;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a home screen cluster.
+ */
+public class Cluster {
+
+    private long mId;
+    private String mName;
+    private CharSequence mDisplayName;
+    private int mImportance;
+    private int mVisibleCount;
+    private boolean mImageCropAllowed;
+    private long mCacheTimeMs;
+    private Intent mIntent;
+
+    private List<ClusterItem> mClusterItems;
+
+    /**
+     * An item displayed inside a cluster.
+     */
+    public static class ClusterItem {
+        private Uri mImageUri;
+
+        ClusterItem(Uri imageUri) {
+            mImageUri = imageUri;
+        }
+
+        public Uri getImageUri() {
+            return mImageUri;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("imageUri: ").append(mImageUri);
+            return builder.toString();
+        }
+    }
+
+    public Cluster() {
+        mClusterItems = new ArrayList<ClusterItem>();
+        mImageCropAllowed = true;
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public CharSequence getDisplayName() {
+        return mDisplayName;
+    }
+
+    public int getImportance() {
+        return mImportance;
+    }
+
+    public int getVisibleCount() {
+        return mVisibleCount;
+    }
+
+    public boolean isImageCropAllowed() {
+        return mImageCropAllowed;
+    }
+
+    public long getCacheTimeMs() {
+        return mCacheTimeMs;
+    }
+
+    public Intent getIntent() {
+        return mIntent;
+    }
+
+    public int getItemCount() {
+        return mClusterItems.size();
+    }
+
+    public ClusterItem getItem(int position) {
+        if (position >= 0 && position < mClusterItems.size()) {
+            return mClusterItems.get(position);
+        }
+        return null;
+    }
+
+    void addClusterItem(ClusterItem item) {
+        mClusterItems.add(item);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("id: ").append(mId)
+                .append(", name: ").append(mName)
+                .append(", displayName: ").append(mDisplayName)
+                .append(", importance: ").append(mImportance)
+                .append(", visibleCount: ").append(mVisibleCount)
+                .append(", imageCropAllowed: ").append(mImageCropAllowed)
+                .append(", cacheTimeMs: ").append(mCacheTimeMs)
+                .append(", intent: ").append(mIntent.toUri(0));
+        return builder.toString();
+    }
+
+    /**
+     * Builds cluster objects.
+     */
+    public static class Builder {
+        private long mId;
+        private String mName;
+        private CharSequence mDisplayName;
+        private int mImportance;
+        private int mVisibleCount;
+        private boolean mImageCropAllowed;
+        private long mCacheTimeMs;
+        private Intent mIntent;
+
+        private List<ClusterItem> mClusterItems;
+
+        public Cluster build() {
+            Cluster cluster = new Cluster();
+            cluster.mId = mId;
+            cluster.mName = mName;
+            cluster.mDisplayName = mDisplayName;
+            cluster.mImportance = mImportance;
+            cluster.mVisibleCount = mVisibleCount;
+            cluster.mImageCropAllowed = mImageCropAllowed;
+            cluster.mIntent = mIntent;
+            cluster.mCacheTimeMs = mCacheTimeMs;
+            cluster.mClusterItems.addAll(mClusterItems);
+            return cluster;
+        }
+
+        public Builder() {
+            mClusterItems = new ArrayList<ClusterItem>();
+            mImageCropAllowed = true;
+        }
+
+        public Builder id(long id) {
+            mId = id;
+            return this;
+        }
+
+        public Builder name(String name) {
+            mName = name;
+            return this;
+        }
+
+        public Builder displayName(CharSequence displayName) {
+            mDisplayName = displayName;
+            return this;
+        }
+
+        public Builder importance(int importance) {
+            mImportance = importance;
+            return this;
+        }
+
+        public Builder visibleCount(int visibleCount) {
+            mVisibleCount = visibleCount;
+            return this;
+        }
+
+        public Builder imageCropAllowed(boolean allowed) {
+            mImageCropAllowed = allowed;
+            return this;
+        }
+
+        public Builder cacheTimeMs(long cacheTimeMs) {
+            mCacheTimeMs = cacheTimeMs;
+            return this;
+        }
+
+        public Builder intent(Intent intent) {
+            mIntent = intent;
+            return this;
+        }
+
+        public Builder addItem(Uri imageUri) {
+            ClusterItem item = new ClusterItem(imageUri);
+            mClusterItems.add(item);
+            return this;
+        }
+    }
+}
diff --git a/src/com/google/android/canvas/data/util/UriUtils.java b/src/com/google/android/canvas/data/util/UriUtils.java
new file mode 100644 (file)
index 0000000..7b7b73c
--- /dev/null
@@ -0,0 +1,124 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.android.canvas.data.util;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent.ShortcutIconResource;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+
+/**
+ * Utilities for working with URIs.
+ */
+public final class UriUtils {
+
+    private static final String SCHEME_SHORTCUT_ICON_RESOURCE = "shortcut.icon.resource";
+    private static final String SCHEME_DELIMITER = "://";
+    private static final String URI_PATH_DELIMITER = "/";
+    private static final String URI_PACKAGE_DELIMITER = ":";
+    private static final String HTTP_PREFIX = "http";
+    private static final String HTTPS_PREFIX = "https";
+
+    /**
+     * Non instantiable.
+     */
+    private UriUtils() {}
+
+    /**
+     * get resource uri representation for a resource of a package
+     */
+    public static String getAndroidResourceUri(Context context, int resourceId) {
+        return getAndroidResourceUri(context.getResources(), resourceId);
+    }
+
+    /**
+     * get resource uri representation for a resource
+     */
+    public static String getAndroidResourceUri(Resources resources, int resourceId) {
+        return ContentResolver.SCHEME_ANDROID_RESOURCE
+                + SCHEME_DELIMITER + resources.getResourceName(resourceId)
+                        .replace(URI_PACKAGE_DELIMITER, URI_PATH_DELIMITER);
+    }
+
+    /**
+     * load drawable from resource
+     * TODO: move to a separate class to handle bitmap and drawables
+     */
+    public static Drawable getDrawable(Context context, ShortcutIconResource r)
+            throws NameNotFoundException {
+        Resources resources = context.getPackageManager().getResourcesForApplication(r.packageName);
+        if (resources == null) {
+            return null;
+        }
+        final int id = resources.getIdentifier(r.resourceName, null, null);
+        return resources.getDrawable(id);
+    }
+
+    /**
+     * Gets a URI with short cut icon scheme.
+     */
+    public static Uri getShortcutIconResourceUri(ShortcutIconResource iconResource) {
+        return Uri.parse(SCHEME_SHORTCUT_ICON_RESOURCE + SCHEME_DELIMITER + iconResource.packageName
+                + URI_PATH_DELIMITER
+                + iconResource.resourceName.replace(URI_PACKAGE_DELIMITER, URI_PATH_DELIMITER));
+    }
+
+    /**
+     * Gets a URI with scheme = {@link ContentResolver#SCHEME_ANDROID_RESOURCE}.
+     */
+    public static Uri getAndroidResourceUri(String resourceName) {
+        Uri uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + SCHEME_DELIMITER
+                + resourceName.replace(URI_PACKAGE_DELIMITER, URI_PATH_DELIMITER));
+        return uri;
+    }
+
+    /**
+     * Checks if the URI refers to an Android resource.
+     */
+    public static boolean isAndroidResourceUri(Uri uri) {
+        return ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme());
+    }
+
+    /**
+     * Checks if the URI refers to an shortcut icon resource.
+     */
+    public static boolean isShortcutIconResourceUri(Uri uri) {
+        return SCHEME_SHORTCUT_ICON_RESOURCE.equals(uri.getScheme());
+    }
+
+    /**
+     * Creates a shortcut icon resource object from an Android resource URI.
+     */
+    public static ShortcutIconResource getIconResource(Uri uri) {
+        if(isAndroidResourceUri(uri)) {
+            ShortcutIconResource iconResource = new ShortcutIconResource();
+            iconResource.packageName = uri.getAuthority();
+            // Trim off the scheme + 3 extra for "://", then replace the first "/" with a ":"
+            iconResource.resourceName = uri.toString().substring(
+                    ContentResolver.SCHEME_ANDROID_RESOURCE.length() + SCHEME_DELIMITER.length())
+                    .replaceFirst(URI_PATH_DELIMITER, URI_PACKAGE_DELIMITER);
+            return iconResource;
+        } else if(isShortcutIconResourceUri(uri)) {
+            ShortcutIconResource iconResource = new ShortcutIconResource();
+            iconResource.packageName = uri.getAuthority();
+            iconResource.resourceName = uri.toString().substring(
+                    SCHEME_SHORTCUT_ICON_RESOURCE.length() + SCHEME_DELIMITER.length()
+                    + iconResource.packageName.length() + URI_PATH_DELIMITER.length())
+                    .replaceFirst(URI_PATH_DELIMITER, URI_PACKAGE_DELIMITER);
+            return iconResource;
+        } else {
+            throw new IllegalArgumentException("Invalid resource URI. " + uri);
+        }
+    }
+
+    /**
+     * Returns {@code true} if this is a web URI.
+     */
+    public static boolean isWebUri(Uri resourceUri) {
+        String scheme = resourceUri.getScheme().toLowerCase();
+        return HTTP_PREFIX.equals(scheme) || HTTPS_PREFIX.equals(scheme);
+    }
+}
diff --git a/src/com/google/android/canvas/provider/CanvasContract.java b/src/com/google/android/canvas/provider/CanvasContract.java
new file mode 100644 (file)
index 0000000..f6a1741
--- /dev/null
@@ -0,0 +1,895 @@
+package com.google.android.canvas.provider;
+
+import android.content.ContentUris;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+/**
+ * The contract between Canvas and ContentProviders that allow access to Canvas
+ * browsing data. All apps that wish to interact with Canvas should use these
+ * definitions.
+ *
+ * TODO add more details
+ */
+public final class CanvasContract {
+
+    // Base for content uris
+    public static final String CONTENT = "content://";
+    /**
+     * Path to items within a single cluster in the launcher. This can be used
+     * when setting up a UriMatcher. Queries will use the form
+     * content://<authority>/<path>/<#> where the # is the _id of the cluster
+     * being queried
+     */
+    public static final String PATH_LAUNCHER_ITEM = "items";
+    /**
+     * Path to the header meta-data. This can be used when setting up a
+     * UriMatcher. Queries will use the form content://<authority>/<path>
+     */
+    public static final String PATH_BROWSE_HEADERS = "headers";
+
+    /**
+     * This tag is used to identify the authority to be used for a canvas
+     * launcher app.
+     *
+     * TODO: this is obsolete: remove.
+     */
+    public static final String METADATA_TAG = "com.google.android.canvas.data.launcher";
+
+    /**
+     * This tag is used to identify the launcher info data file to be used for a canvas
+     * launcher app.
+     */
+    public static final String METADATA_LAUNCHER_INFO_TAG =
+            "com.google.android.canvas.data.launcher_info";
+
+    /**
+     * This tag is used to denote a background color hint for the activity.
+     * <p>
+     * This can either be a reference to an @color or else a string (e.g. #ff001100).
+     *
+     * TODO: this is obsolete: remove.
+     */
+    public static final String METADATA_COLOR_HINT =
+            "com.google.android.canvas.ui.launcher_color_hint";
+
+    /**
+     * An intent action for browsing app content in Canvas. Apps receiving this
+     * intent should call {@link Intent#getData()} to retrieve the base Uri and
+     * {@link #EXTRA_START_INDEX} or {@link #EXTRA_START_ID} to find which header
+     * to start at (default 0).
+     */
+    public static final String ACTION_BROWSE = "com.google.android.canvas.action.BROWSE";
+
+    /**
+     * The index of the header to focus on initially when the browse is launched.
+     * This extra is optional and defaults to 0. If {@link #EXTRA_START_ID} is present
+     * this value will not be used.
+     */
+    public static final String EXTRA_START_INDEX = "start_index";
+
+    /**
+     * The _id of the header to focus on initially when the browse is launched.
+     * This extra is optional and {@link #EXTRA_START_ID} is used by default.
+     */
+    public static final String EXTRA_START_ID = "start_id";
+
+    /**
+     * An intent action for viewing detail content in Canvas. Apps receiving this
+     * intent should call {@link Intent#getData()} to retrieve the base Uri.
+     */
+    public static final String ACTION_DETAIL = "com.google.android.canvas.action.DETAIL";
+
+    /**
+     * Path for querying details for an item.
+     */
+    public static final String PATH_DETAIL_ITEM = "details";
+
+    /**
+     * Path for querying sections for a detail item. This can be used when setting up a
+     * UriMatcher. Queries will use the form content://<authority>/details/<item_id>/sections.
+     */
+    public static final String PATH_DETAIL_SECTIONS = "sections";
+
+    /**
+     * Path for querying detail actions. This can be used when setting up a UriMatcher. Queries will
+     * use the form content://<authority>/details/<item_id>/actions.
+     */
+    public static final String PATH_DETAIL_ACTIONS = "actions";
+
+    /**
+     * Action for searching a Canvas provider. Apps receiving this
+     * intent should call {@link Intent#getData()} to retrieve the base Uri and
+     * {@link #EXTRA_QUERY} to find query.
+     */
+    public static final String ACTION_SEARCH = "com.google.android.canvas.action.SEARCH";
+
+    /**
+     * The query to be executed when search activity is launched
+     * This extra is optional and defaults to null.
+     */
+    public static final String EXTRA_QUERY = "query";
+
+    /**
+     * Optional int extra for setting the display mode of the search activity.
+     *
+     * @see #DISPLAY_MODE_ROW
+     * @see #DISPLAY_MODE_GRID
+     */
+    public static final String EXTRA_DISPLAY_MODE = "display_mode";
+
+    public static final int DISPLAY_MODE_ROW = 0;
+    public static final int DISPLAY_MODE_GRID = 1;
+
+    /**
+     * Value for the root Canvas URI when this activity should be excluded from the Canvas top level
+     * and the legacy apps area.
+     *
+     * TODO: this is obsolete: remove.
+     */
+    public static final String EXCLUDED_ROOT_URI = "excluded";
+
+    protected interface LauncherColumns {
+
+        /**
+         * The name of the cluster. Generally used for debugging and not shown
+         * to the user.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String NAME = "name";
+
+        /**
+         * An optional name to display with the cluster. This value will be user
+         * visible. Example: "Recently Watched"
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_NAME = "display_name";
+
+        /**
+         * How important this cluster is. The higher the value the more important
+         * it will be relative to other clusters. The importance is a relative
+         * weighting and not an absolute priority. If this value is left blank
+         * it will default to 0 (not important).
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String IMPORTANCE = "importance";
+
+        /**
+         * The number of items that should be shown in this cluster. If this
+         * value is more than there is space for fewer items may be shown.
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String VISIBLE_COUNT = "visible_count";
+
+        /**
+         * Whether or not the image may be cropped to fit the display area
+         * better. 1 means cropping is allowed, 0 means the item should be
+         * shrunk or stretched to fit instead.
+         *
+         * <P>Type: INTEGER (0 or 1)</P>
+         */
+        public static final String IMAGE_CROP_ALLOWED = "image_crop_allowed";
+
+        /**
+         * The amount of time it is safe to assume the data for this
+         * cluster will remain valid. For example, if this cluster is
+         * advertising a daily special this should return the time until the
+         * special ends. A best effort will be made to not display data past
+         * this point but some data may not be requeried immediately.
+         *
+         * <P>Type: INTEGER (long)</P>
+         */
+        public static final String CACHE_TIME_MS = "cache_time_ms";
+
+        /**
+         * A standard Intent Uri to be launched when this cluster is selected.
+         * This may be a {@link CanvasContract#ACTION_BROWSE} intent or an
+         * intent to launch directly into an app. You can also use
+         * {@link CanvasContract#getBrowseIntent(Uri, int)} to generate a
+         * browse intent for a given root Uri. Use {@link Intent#toUri(int)}
+         * with a flag of {@link Intent#URI_INTENT_SCHEME}.
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String INTENT_URI = "intent_uri";
+
+        /**
+         * A String to display as a notification on the launcher. This may also
+         * cause a visual indication to be shown on the launcher when this app
+         * is not in view.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String NOTIFICATION_TEXT = "notification_text";
+
+        /**
+         * An optional Uri for querying progresss for any ongoing actions, such
+         * as an active download.
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String PROGRESS_URI = "progress_uri";
+
+    }
+
+    public static final class Launcher implements BaseColumns, LauncherColumns {
+
+        /**
+         * This utility class cannot be instantiated
+         */
+        private Launcher() {}
+    }
+
+    protected interface ProgressColumns {
+        /**
+         * The current progress as an integer in the range [0-100] inclusive.
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String PROGRESS = "progress";
+    }
+
+    public static final class Progress implements BaseColumns, ProgressColumns {
+        /**
+         * This utility class cannot be instantiated
+         */
+        private Progress() {}
+    }
+
+    protected interface LauncherItemColumns {
+        /**
+         * The _id of the cluster this item is a member of.
+         *
+         * <P>Type: INTEGER (long)</P>
+         */
+        public static final String PARENT_ID = "parent_id";
+
+        /**
+         * The uri for retrieving the image to show for this item. This
+         * String should be generated using {@link Uri#toString()}
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String IMAGE_URI = "image_uri";
+
+    }
+
+    public static final class LauncherItem implements BaseColumns, LauncherItemColumns {
+
+        /**
+         * Returns a Uri that can be used to query the individual items in a
+         * cluster.
+         *
+         * @param root The Uri base path to query against.
+         * @param clusterId the _id of the cluster returned by querying the Launcher
+         *            Uri for this provider.
+         */
+        public static final Uri getLauncherItemsUri(Uri root, long clusterId) {
+            return Uri.withAppendedPath(root, PATH_LAUNCHER_ITEM + "/" + clusterId);
+        }
+
+        /**
+         * Returns a Uri that can be used to query the all items across clusters.
+         *
+         * @param root The ContentProvider authority that this Uri should query
+         *            against.
+         */
+        public static final Uri getLauncherItemsUri(Uri root) {
+            return Uri.withAppendedPath(root, PATH_LAUNCHER_ITEM);
+        }
+
+        /**
+         * This utility class cannot be instantiated
+         */
+        private LauncherItem() {};
+    }
+
+    protected interface BrowseHeadersColumns {
+        /**
+         * Reference name of the header, not used for display to the users.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String NAME = "name";
+
+        /**
+         * The name to show for the header. User visible.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_NAME = "display_name";
+
+        /**
+         * Uri pointing to an icon to be used as part of the header. This
+         * String should be generated using {@link Uri#toString()}
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String ICON_URI = "icon_uri";
+
+        /**
+         * Uri pointing to an icon to be used for app branding on this tab.
+         * This String should be generated using {@link Uri#toString()}
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String BADGE_URI = "badge_uri";
+
+        /**
+         * A 0xAARRGGBB color that should be applied to the background when on
+         * this tab.
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String COLOR_HINT = "color_hint";
+
+        /**
+         * A 0xAARRGGBB color that should be applied to the text when on this
+         * tab.
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String TEXT_COLOR_HINT = "text_color_hint";
+
+        /**
+         * Uri pointing to an image to display in the background when on this
+         * tab. Be sure the image contrasts enough with the text color hint and
+         * is of high enough quality to be displayed at 1080p. This String
+         * should be generated using {@link Uri#toString()}.  The URI will be either
+         * a resource uri in format of android:resource:// or an external URL
+         * like file://, http://, https://.
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String BG_IMAGE_URI = "bg_image_uri";
+
+        /**
+         * The default width of the expanded image
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String DEFAULT_ITEM_WIDTH = "default_item_width";
+
+        /**
+         * The default height of the expanded image
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String DEFAULT_ITEM_HEIGHT = "default_item_height";
+
+        /**
+         * 1 to show a lane below images for description, 0 to hide.
+         * Default value is 1.
+         *
+         * <P>Type: INTEGER (0 or 1)</P>
+         */
+        public static final String SHOW_DESCRIPTIONS = "show_descriptions";
+
+        /**
+         * A group id. If this is not 0, contiguous headers with the same
+         * expand group will be expanded together. Non-contiguous headers with
+         * the same expand group is an error.
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String EXPAND_GROUP = "expand_group";
+
+        /**
+         * Controls whether the items in this row will wrap around back to the
+         * beginning when the user scrolls to the last item. 0 to not wrap items,
+         * 1 to wrap.
+         *
+         * <P>Type: INTEGER (0 or 1)</P>
+         */
+        public static final String WRAP_ITEMS = "wrap_items";
+
+    }
+
+    public static final class BrowseHeaders implements BaseColumns, BrowseHeadersColumns {
+        /**
+         * Returns a uri for retrieving a list of browse header meta-data items that
+         * describe the categories for this browse path (name, badge, color hint,
+         * background image, etc.)
+         *
+         * @param root The base content Uri to browse.
+         * @return
+         */
+        public static final Uri getBrowseHeadersUri(Uri root) {
+            return Uri.withAppendedPath(root, PATH_BROWSE_HEADERS);
+        }
+
+        /**
+         * This utility class cannot be instantiated
+         */
+        private BrowseHeaders(){}
+    }
+
+    protected interface BrowseItemsColumns {
+        /**
+         * The _id of the header this item belongs to.
+         *
+         * <P>Type: INTEGER (long)</P>
+         */
+        public static final String PARENT_ID = "parent_id";
+
+        /**
+         * Text that may be shown to the user along with the image or instead
+         * of an image if no image was specified.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_NAME = "display_name";
+
+        /**
+         * Long description text of this item.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_DESCRIPTION = "display_description";
+
+        /**
+         * The uri for retrieving the image to show for this item. This string
+         * should be created using {@link Uri#toString()}.
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String IMAGE_URI = "image_uri";
+
+        /**
+         * The width of the image for this item in pixels.
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String WIDTH = "width";
+
+        /**
+         * The height of the image for this item in pixels.
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String HEIGHT = "height";
+
+        /**
+         * An intent to launch when this item is selected. It may be another
+         * browse intent or a deep link into the app. This String should be
+         * generated using {@link Intent#toUri(int)} with
+         * {@link Intent#URI_INTENT_SCHEME}.
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String INTENT_URI = "intent_uri";
+    }
+
+    public static final class BrowseItems implements BaseColumns, BrowseItemsColumns {
+        /**
+         * Returns a Uri that can be used to query the items within a browse
+         * category.
+         *
+         * @param root The base content Uri that is being browsed.
+         * @param headerId The _id of the header that will be queried.
+         * @return
+         */
+        public static final Uri getBrowseItemsUri(Uri root, long headerId) {
+            return ContentUris.withAppendedId(root, headerId);
+        }
+    }
+
+    protected interface UserRatingColumns {
+        /**
+         * The average rating for this item. (Optional)
+         *
+         * <P>Type: Double</P>
+         */
+        public static final String USER_RATING_AVERAGE = "user_rating_average";
+
+        /**
+         * A simple rating for this item as an integer in the range
+         * [0-10] inclusive. (Optional)
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String USER_RATING_SIMPLE = "user_rating_simple";
+
+        /**
+         * The number of reviews included in the average rating. (Optional)
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String USER_RATING_COUNT = "user_rating_count";
+    }
+
+    protected interface DetailItemColumns {
+
+        /**
+         * Title of the item.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_NAME = "display_name";
+
+        /**
+         * Long description text of this item.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_DESCRIPTION = "display_description";
+
+        /**
+         * The uri for retrieving the foreground image to show for this item. This string
+         * should be created using {@link Uri#toString()}.
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String FOREGROUND_IMAGE_URI = "foreground_image_uri";
+
+        /**
+         * The uri for retrieving the background image to show for this item. This string
+         * should be created using {@link Uri#toString()}.
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String BACKGROUND_IMAGE_URI = "background_image_uri";
+
+        /**
+         * A 0xAARRGGBB color that should be applied to the background.
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String COLOR_HINT = "color_hint";
+
+        /**
+         * The uri for the badge
+         */
+        public static final String BADGE_URI = "badge_uri";
+
+        /**
+         * A 0xAARRGGBB color that should be applied to rendered text so as no
+         * not conflict with the {@link #COLOR_HINT} or
+         * {@link #BACKGROUND_IMAGE_URI}.
+         *
+         *<P>Type: INTEGER</P>
+         */
+        public static final String TEXT_COLOR_HINT = "text_color_hint";
+    }
+
+    public static final class DetailItem implements BaseColumns, DetailItemColumns {
+
+        /**
+         * Non instantiable.
+         */
+        private DetailItem() {}
+    }
+
+    protected interface DetailSectionsColumns {
+
+        /**
+         * Text that will be shown to the user for navigating between sections.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_HEADER = "display_header";
+
+        /**
+         * Primary text for display when a section is visible, such as
+         * an artist name or movie title. (Optional)
+         * <p>
+         * This is only valid if {@link #SECTION_TYPE} is
+         * {@link #SECTION_TYPE_LIST} or {@link #SECTION_TYPE_SECTIONS}.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_NAME = "display_name";
+
+        /**
+         * Secondary text for display when a section is visible, such as
+         * a release date or album title. (Optional)
+         * <p>
+         * This is only valid if {@link #SECTION_TYPE} is
+         * {@link #SECTION_TYPE_LIST} or {@link #SECTION_TYPE_SECTIONS}.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_SUBNAME = "display_subname";
+
+        /**
+         * Type of item.
+         * <p>
+         * One of {@link #SECTION_TYPE_BLOB}, {@link #SECTION_TYPE_LIST},
+         * {@link #SECTION_TYPE_BLURB}, {@link #SECTION_TYPE_REVIEWS}, or
+         * {@link #SECTION_TYPE_SECTIONS}.
+         *
+         * <P>Type: Integer</P>
+         */
+        public static final String SECTION_TYPE = "section_type";
+
+        /**
+         * Value for {@link #SECTION_TYPE} if this section has HTML content.
+         * This should only be used as a last resort if the other formats can't
+         * be made to work.
+         */
+        public static final int SECTION_TYPE_BLOB = 0;
+
+        /**
+         * Value for {@link #SECTION_TYPE} if this section uses the default list
+         * formatting for its content.
+         */
+        public static final int SECTION_TYPE_LIST = 1;
+
+        /**
+         * Value for {@link #SECTION_TYPE} if this section uses the default
+         * formatting for its content.
+         */
+        public static final int SECTION_TYPE_BLURB = 2;
+
+        /**
+         * Value for {@link #SECTION_TYPE} if this section has review style
+         * formatting for its content.
+         */
+        public static final int SECTION_TYPE_REVIEWS = 3;
+
+        /**
+         * Value for {@link #SECTION_TYPE} if this section is multiple related
+         * sections that can be grouped. For example, seasons of a TV show
+         * should use this type.
+         */
+        public static final int SECTION_TYPE_SECTIONS = 4;
+
+        /**
+         * Blob content, either a string or HTML.
+         * <p>
+         * If HTML, this will be sanitized before displaying in a web view.
+         * <p>
+         * JavaScript is not allowed.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String BLOB_CONTENT = "blob_content";
+
+        /**
+         * Action or list of actions available for this section.
+         * <p>
+         * Only valid if {@link #SECTION_TYPE} = {@link #SECTION_TYPE_BLOB}.
+         * <p>
+         * This is either a single intent URI or a content URI pointing to a list of
+         * {@link DetailActions}.
+         */
+        public static final String ACTION_URI = "action_uri";
+
+        /**
+         * Content URI. This must be nested under the detail item ID.
+         * <p>
+         * Only valid if {@link #SECTION_TYPE} is one of
+         * {@link #SECTION_TYPE_LIST}, {@link #SECTION_TYPE_REVIEWS},
+         * {@link #SECTION_TYPE_SECTIONS}.
+         */
+        public static final String CONTENT_URI = "content_uri";
+    }
+
+    /**
+     * A top level listing of the sections for this item, such as "Ratings" or
+     * "Related." The BLOB_CONTENT column is only valid if
+     * {@link DetailSectionsColumns#SECTION_TYPE} =
+     * {@link DetailSectionsColumns#SECTION_TYPE_BLOB}. USER_RATING columns are
+     * only valid if {@link DetailSectionsColumns#SECTION_TYPE} =
+     * {@link DetailSectionsColumns#SECTION_TYPE_REVIEWS}.
+     */
+    public static final class DetailSections
+            implements BaseColumns, DetailSectionsColumns, UserRatingColumns {
+        /**
+         * Non instantiable.
+         */
+        private DetailSections() {}
+
+        /**
+         * Gets a content URI suitable for loading the sections for an item.
+         */
+        public static Uri getSectionsUri(Uri itemUri) {
+            return itemUri.buildUpon().appendPath(PATH_DETAIL_SECTIONS).build();
+        }
+    }
+
+    protected interface DetailBlurbColumns {
+        /**
+         * Primary text that will be shown to the user, generally the title.
+         * (Optional)
+         *
+         *<P>Type: String</P>
+         */
+        public static final String DISPLAY_NAME = "display_name";
+
+        /**
+         * A secondary title with more importance than the description, such as
+         * the artist or director. (Optional)
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_SUBNAME = "display_subname";
+
+        /**
+         * Long description text of this item. (Optional)
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_DESCRIPTION = "display_description";
+
+        /**
+         * The category of the item to show to the user, such as "Apps" or
+         * "Folk". (Optional)
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_CATEGORY = "display_category";
+
+        /**
+         * The URI for retrieving an image to show. This should be a rating
+         * badge or other similar icon. This string should be created using
+         * {@link Uri#toString()}. (Optional)
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String BADGE_URI = "badge_uri";
+    }
+
+    public static final class DetailBlurb
+            implements BaseColumns, DetailBlurbColumns, UserRatingColumns {
+        /**
+         * Non instantiable.
+         */
+        private DetailBlurb() {}
+    }
+
+    protected interface ItemChildrenColumns {
+        /**
+         * Primary text that will be shown to the user, generally the title.
+         * (Optional)
+         *
+         *<P>Type: String</P>
+         */
+        public static final String DISPLAY_NAME = "display_name";
+
+        /**
+         * A secondary title with more importance than the description, such as
+         * the artist or director. (Optional)
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_SUBNAME = "display_subname";
+
+        /**
+         * Long description text of this item. (Optional)
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_DESCRIPTION = "display_description";
+
+        /**
+         * A number to display next to the item, such as the track or
+         * episode number. 0 is not allowed. (Optional)
+         *
+         * <P>Type: INTEGER</P>
+         */
+        public static final String DISPLAY_NUMBER = "display_number";
+
+        /**
+         * The uri for retrieving the image to show for this item. This string
+         * should be created using {@link Uri#toString()}. (Optional)
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String IMAGE_URI = "image_uri";
+
+        /**
+         * Either an intent URI for an intent that should be triggered or else a content URI
+         * pointing to a list of actions for this item.
+         * <p>
+         * If the list has only 1 action per item, it is more efficient to supply an intent URI
+         * here. (Optional)
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String ACTION_URI = "action_uri";
+    }
+
+    public static final class DetailChildren
+            implements BaseColumns, ItemChildrenColumns, UserRatingColumns {
+
+        /**
+         * Non instantiable.
+         */
+        private DetailChildren() {}
+    }
+
+    public static final class SearchResults
+            implements BaseColumns, ItemChildrenColumns, UserRatingColumns {
+
+        /**
+         * Non instantiable.
+         */
+        private SearchResults() {}
+    }
+
+    protected interface DetailActionsColumns {
+
+        /**
+         * Text that will be shown to the user.
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_NAME = "display_name";
+
+        /**
+         * Secondary text that will be shown to the user, such as "from $2.99".
+         * (Optional)
+         *
+         * <P>Type: String</P>
+         */
+        public static final String DISPLAY_SUBNAME = "display_subname";
+
+        /**
+         * Intent URI of the form intent:// which will be triggered.
+         *
+         * <P>Type: String (Uri)</P>
+         */
+        public static final String INTENT_URI = "intent_uri";
+    }
+
+    public static final class DetailActions implements BaseColumns, DetailActionsColumns {
+
+        /**
+         * Non instantiable.
+         */
+        private DetailActions() {}
+    }
+
+    /**
+     * Returns a browse intent with the root included. The root should be a
+     * base Uri that the browse queries can build off of.
+     *
+     * Example: "content://com.google.movies/browse/action"
+     *
+     * @param root The authority + path that will be browsed on.
+     * @param start The index of the header to display expanded first.
+     * @return
+     */
+    public static Intent getBrowseIntent(Uri root, int start) {
+        Intent intent = new Intent(ACTION_BROWSE);
+        intent.setData(root);
+        intent.putExtra(EXTRA_START_INDEX, start);
+        return intent;
+    }
+
+    /**
+     * Returns a browse intent with the root included. The root should be a
+     * base Uri that the browse queries can build off of.
+     *
+     * Example: "content://com.google.movies/browse/action"
+     *
+     * @param root The authority + path that will be browsed on.
+     * @param startId The _id of the header to display expanded first.
+     * @return
+     */
+    public static Intent getBrowseIntentById(Uri root, long startId) {
+        Intent intent = new Intent(ACTION_BROWSE);
+        intent.setData(root);
+        intent.putExtra(EXTRA_START_ID, startId);
+        return intent;
+    }
+
+    /**
+     * Returns a details intent for the given root URI. The root should be a URI
+     * specific to the item being viewed that can be appended to for details
+     * queries.
+     *
+     * Example: "content://com.google.movies/details/75289"
+     */
+    public static Intent getDetailsIntent(Uri root) {
+        Intent intent = new Intent(ACTION_DETAIL);
+        intent.setData(root);
+        return intent;
+    }
+}
\ No newline at end of file