diff options
-rw-r--r-- | src/com/android/gallery3d/provider/CanvasProvider.java | 660 | ||||
-rw-r--r-- | src/com/google/android/canvas/data/Cluster.java | 195 | ||||
-rw-r--r-- | src/com/google/android/canvas/data/util/UriUtils.java | 124 | ||||
-rw-r--r-- | src/com/google/android/canvas/provider/CanvasContract.java | 895 |
4 files changed, 1874 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/provider/CanvasProvider.java b/src/com/android/gallery3d/provider/CanvasProvider.java new file mode 100644 index 000000000..86c72fb3c --- /dev/null +++ b/src/com/android/gallery3d/provider/CanvasProvider.java @@ -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 index 000000000..ab6aaedcc --- /dev/null +++ b/src/com/google/android/canvas/data/Cluster.java @@ -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 index 000000000..7b7b73cb5 --- /dev/null +++ b/src/com/google/android/canvas/data/util/UriUtils.java @@ -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 index 000000000..f6a1741c4 --- /dev/null +++ b/src/com/google/android/canvas/provider/CanvasContract.java @@ -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 |