| /* |
| * Copyright (C) 2015 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.example.android.tvleanback.data; |
| |
| import android.app.SearchManager; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.database.sqlite.SQLiteOpenHelper; |
| import android.database.sqlite.SQLiteQueryBuilder; |
| import android.media.Rating; |
| import android.provider.BaseColumns; |
| import android.util.Log; |
| |
| import com.example.android.tvleanback.R; |
| import com.example.android.tvleanback.model.Movie; |
| |
| import org.json.JSONException; |
| |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Contains logic to return specific words from the video database, and |
| * load the video database table when it needs to be created. |
| */ |
| public class VideoDatabase { |
| //The columns we'll include in the video database table |
| public static final String KEY_NAME = SearchManager.SUGGEST_COLUMN_TEXT_1; |
| public static final String KEY_DESCRIPTION = SearchManager.SUGGEST_COLUMN_TEXT_2; |
| public static final String KEY_ICON = SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE; |
| public static final String KEY_DATA_TYPE = SearchManager.SUGGEST_COLUMN_CONTENT_TYPE; |
| public static final String KEY_IS_LIVE = SearchManager.SUGGEST_COLUMN_IS_LIVE; |
| public static final String KEY_VIDEO_WIDTH = SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH; |
| public static final String KEY_VIDEO_HEIGHT = SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT; |
| public static final String KEY_AUDIO_CHANNEL_CONFIG = |
| SearchManager.SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG; |
| public static final String KEY_PURCHASE_PRICE = SearchManager.SUGGEST_COLUMN_PURCHASE_PRICE; |
| public static final String KEY_RENTAL_PRICE = SearchManager.SUGGEST_COLUMN_RENTAL_PRICE; |
| public static final String KEY_RATING_STYLE = SearchManager.SUGGEST_COLUMN_RATING_STYLE; |
| public static final String KEY_RATING_SCORE = SearchManager.SUGGEST_COLUMN_RATING_SCORE; |
| public static final String KEY_PRODUCTION_YEAR = SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR; |
| public static final String KEY_COLUMN_DURATION = SearchManager.SUGGEST_COLUMN_DURATION; |
| public static final String KEY_ACTION = SearchManager.SUGGEST_COLUMN_INTENT_ACTION; |
| private static final String TAG = "VideoDatabase"; |
| private static final String DATABASE_NAME = "video_database_leanback"; |
| private static final String FTS_VIRTUAL_TABLE = "Leanback_table"; |
| private static final int DATABASE_VERSION = 2; |
| private static final HashMap<String, String> COLUMN_MAP = buildColumnMap(); |
| private static int CARD_WIDTH = 313; |
| private static int CARD_HEIGHT = 176; |
| private final VideoDatabaseOpenHelper mDatabaseOpenHelper; |
| |
| /** |
| * Constructor |
| * |
| * @param context The Context within which to work, used to create the DB |
| */ |
| public VideoDatabase(Context context) { |
| mDatabaseOpenHelper = new VideoDatabaseOpenHelper(context); |
| } |
| |
| /** |
| * Builds a map for all columns that may be requested, which will be given to the |
| * SQLiteQueryBuilder. This is a good way to define aliases for column names, but must include |
| * all columns, even if the value is the key. This allows the ContentProvider to request |
| * columns w/o the need to know real column names and create the alias itself. |
| */ |
| private static HashMap<String, String> buildColumnMap() { |
| HashMap<String, String> map = new HashMap<String, String>(); |
| map.put(KEY_NAME, KEY_NAME); |
| map.put(KEY_DESCRIPTION, KEY_DESCRIPTION); |
| map.put(KEY_ICON, KEY_ICON); |
| map.put(KEY_DATA_TYPE, KEY_DATA_TYPE); |
| map.put(KEY_IS_LIVE, KEY_IS_LIVE); |
| map.put(KEY_VIDEO_WIDTH, KEY_VIDEO_WIDTH); |
| map.put(KEY_VIDEO_HEIGHT, KEY_VIDEO_HEIGHT); |
| map.put(KEY_AUDIO_CHANNEL_CONFIG, KEY_AUDIO_CHANNEL_CONFIG); |
| map.put(KEY_PURCHASE_PRICE, KEY_PURCHASE_PRICE); |
| map.put(KEY_RENTAL_PRICE, KEY_RENTAL_PRICE); |
| map.put(KEY_RATING_STYLE, KEY_RATING_STYLE); |
| map.put(KEY_RATING_SCORE, KEY_RATING_SCORE); |
| map.put(KEY_PRODUCTION_YEAR, KEY_PRODUCTION_YEAR); |
| map.put(KEY_COLUMN_DURATION, KEY_COLUMN_DURATION); |
| map.put(KEY_ACTION, KEY_ACTION); |
| map.put(BaseColumns._ID, "rowid AS " + |
| BaseColumns._ID); |
| map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, "rowid AS " + |
| SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); |
| map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, "rowid AS " + |
| SearchManager.SUGGEST_COLUMN_SHORTCUT_ID); |
| return map; |
| } |
| |
| /** |
| * Returns a Cursor positioned at the word specified by rowId |
| * |
| * @param rowId id of word to retrieve |
| * @param columns The columns to include, if null then all are included |
| * @return Cursor positioned to matching word, or null if not found. |
| */ |
| public Cursor getWord(String rowId, String[] columns) { |
| /* This builds a query that looks like: |
| * SELECT <columns> FROM <table> WHERE rowid = <rowId> |
| */ |
| String selection = "rowid = ?"; |
| String[] selectionArgs = new String[]{rowId}; |
| |
| return query(selection, selectionArgs, columns); |
| } |
| |
| /** |
| * Returns a Cursor over all words that match the first letter of the given query |
| * |
| * @param query The string to search for |
| * @param columns The columns to include, if null then all are included |
| * @return Cursor over all words that match, or null if none found. |
| */ |
| public Cursor getWordMatch(String query, String[] columns) { |
| /* This builds a query that looks like: |
| * SELECT <columns> FROM <table> WHERE <KEY_WORD> MATCH 'query*' |
| * which is an FTS3 search for the query text (plus a wildcard) inside the word column. |
| * |
| * - "rowid" is the unique id for all rows but we need this value for the "_id" column in |
| * order for the Adapters to work, so the columns need to make "_id" an alias for "rowid" |
| * - "rowid" also needs to be used by the SUGGEST_COLUMN_INTENT_DATA alias in order |
| * for suggestions to carry the proper intent data.SearchManager |
| * These aliases are defined in the VideoProvider when queries are made. |
| * - This can be revised to also search the definition text with FTS3 by changing |
| * the selection clause to use FTS_VIRTUAL_TABLE instead of KEY_WORD (to search across |
| * the entire table, but sorting the relevance could be difficult. |
| */ |
| String selection = KEY_NAME + " MATCH ?"; |
| String[] selectionArgs = new String[]{query + "*"}; |
| |
| return query(selection, selectionArgs, columns); |
| } |
| |
| /** |
| * Performs a database query. |
| * |
| * @param selection The selection clause |
| * @param selectionArgs Selection arguments for "?" components in the selection |
| * @param columns The columns to return |
| * @return A Cursor over all rows matching the query |
| */ |
| private Cursor query(String selection, String[] selectionArgs, String[] columns) { |
| /* The SQLiteBuilder provides a map for all possible columns requested to |
| * actual columns in the database, creating a simple column alias mechanism |
| * by which the ContentProvider does not need to know the real column names |
| */ |
| SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); |
| builder.setTables(FTS_VIRTUAL_TABLE); |
| builder.setProjectionMap(COLUMN_MAP); |
| |
| Cursor cursor = new PaginatedCursor(builder.query(mDatabaseOpenHelper.getReadableDatabase(), |
| columns, selection, selectionArgs, null, null, null)); |
| |
| if (cursor == null) { |
| return null; |
| } else if (!cursor.moveToFirst()) { |
| cursor.close(); |
| return null; |
| } |
| return cursor; |
| } |
| |
| /** |
| * This creates/opens the database. |
| */ |
| private static class VideoDatabaseOpenHelper extends SQLiteOpenHelper { |
| |
| private final Context mHelperContext; |
| private SQLiteDatabase mDatabase; |
| |
| VideoDatabaseOpenHelper(Context context) { |
| super(context, DATABASE_NAME, null, DATABASE_VERSION); |
| mHelperContext = context; |
| } |
| |
| /* Note that FTS3 does not support column constraints and thus, you cannot |
| * declare a primary key. However, "rowid" is automatically used as a unique |
| * identifier, so when making requests, we will use "_id" as an alias for "rowid" |
| */ |
| private static final String FTS_TABLE_CREATE = |
| "CREATE VIRTUAL TABLE " + FTS_VIRTUAL_TABLE + |
| " USING fts3 (" + |
| KEY_NAME + ", " + |
| KEY_DESCRIPTION + "," + |
| KEY_ICON + "," + |
| KEY_DATA_TYPE + "," + |
| KEY_IS_LIVE + "," + |
| KEY_VIDEO_WIDTH + "," + |
| KEY_VIDEO_HEIGHT + "," + |
| KEY_AUDIO_CHANNEL_CONFIG + "," + |
| KEY_PURCHASE_PRICE + "," + |
| KEY_RENTAL_PRICE + "," + |
| KEY_RATING_STYLE + "," + |
| KEY_RATING_SCORE + "," + |
| KEY_PRODUCTION_YEAR + "," + |
| KEY_COLUMN_DURATION + "," + |
| KEY_ACTION + ");"; |
| |
| @Override |
| public void onCreate(SQLiteDatabase db) { |
| mDatabase = db; |
| mDatabase.execSQL(FTS_TABLE_CREATE); |
| loadDatabase(); |
| } |
| |
| /** |
| * Starts a thread to load the database table with words |
| */ |
| private void loadDatabase() { |
| new Thread(new Runnable() { |
| public void run() { |
| try { |
| loadMovies(); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| }).start(); |
| } |
| |
| private void loadMovies() throws IOException { |
| Log.d(TAG, "Loading movies..."); |
| |
| HashMap<String, List<Movie>> movies = null; |
| try { |
| VideoProvider.setContext(mHelperContext); |
| movies = VideoProvider.buildMedia(mHelperContext, |
| mHelperContext.getResources().getString(R.string.catalog_url)); |
| } catch (JSONException e) { |
| Log.e(TAG, "JSon Exception when loading movie", e); |
| } |
| |
| for (Map.Entry<String, List<Movie>> entry : movies.entrySet()) { |
| List<Movie> list = entry.getValue(); |
| for (Movie movie : list) { |
| long id = addMovie(movie); |
| if (id < 0) { |
| Log.e(TAG, "unable to add movie: " + movie.toString()); |
| } |
| } |
| } |
| // add stub movies to illustrate action deep link in search detail |
| // Android TV Search requires that the media’s title, MIME type, production year, |
| // and duration all match exactly to those found from Google’s servers. |
| addMovieForDeepLink(mHelperContext.getString(R.string.noah_title), |
| mHelperContext.getString(R.string.noah_description), |
| R.drawable.noah, |
| 8280000, |
| "2014"); |
| addMovieForDeepLink(mHelperContext.getString(R.string.dragon2_title), |
| mHelperContext.getString(R.string.dragon2_description), |
| R.drawable.dragon2, |
| 6300000, |
| "2014"); |
| addMovieForDeepLink(mHelperContext.getString(R.string.maleficent_title), |
| mHelperContext.getString(R.string.maleficent_description), |
| R.drawable.maleficent, |
| 5820000, |
| "2014"); |
| } |
| |
| /** |
| * Add a movie to the database. |
| * |
| * @return rowId or -1 if failed |
| */ |
| public long addMovie(Movie movie) { |
| ContentValues initialValues = new ContentValues(); |
| initialValues.put(KEY_NAME, movie.getTitle()); |
| initialValues.put(KEY_DESCRIPTION, movie.getDescription()); |
| initialValues.put(KEY_ICON, movie.getCardImageUrl()); |
| initialValues.put(KEY_DATA_TYPE, "video/mp4"); |
| initialValues.put(KEY_IS_LIVE, false); |
| initialValues.put(KEY_VIDEO_WIDTH, CARD_WIDTH); |
| initialValues.put(KEY_VIDEO_HEIGHT, CARD_HEIGHT); |
| initialValues.put(KEY_AUDIO_CHANNEL_CONFIG, "2.0"); |
| initialValues.put(KEY_PURCHASE_PRICE, mHelperContext.getString(R.string.buy_2)); |
| initialValues.put(KEY_RENTAL_PRICE, mHelperContext.getString(R.string.rent_2)); |
| initialValues.put(KEY_RATING_STYLE, Rating.RATING_5_STARS); |
| initialValues.put(KEY_RATING_SCORE, 3.5f); |
| initialValues.put(KEY_PRODUCTION_YEAR, 2014); |
| initialValues.put(KEY_COLUMN_DURATION, 0); |
| initialValues.put(KEY_ACTION, mHelperContext.getString(R.string.global_search)); |
| return mDatabase.insert(FTS_VIRTUAL_TABLE, null, initialValues); |
| } |
| |
| /** |
| * Add an entry to the database for stub deep link. |
| * |
| * @return rowId or -1 if failed |
| */ |
| public long addMovieForDeepLink(String title, String description, int icon, long duration, String production_year) { |
| ContentValues initialValues = new ContentValues(); |
| initialValues.put(KEY_NAME, title); |
| initialValues.put(KEY_DESCRIPTION, description); |
| initialValues.put(KEY_ICON, icon); |
| initialValues.put(KEY_DATA_TYPE, "video/mp4"); |
| initialValues.put(KEY_IS_LIVE, false); |
| initialValues.put(KEY_VIDEO_WIDTH, 1280); |
| initialValues.put(KEY_VIDEO_HEIGHT, 720); |
| initialValues.put(KEY_AUDIO_CHANNEL_CONFIG, "2.0"); |
| initialValues.put(KEY_PURCHASE_PRICE, "Free"); |
| initialValues.put(KEY_RENTAL_PRICE, "Free"); |
| initialValues.put(KEY_RATING_STYLE, Rating.RATING_5_STARS); |
| initialValues.put(KEY_RATING_SCORE, 3.5f); |
| initialValues.put(KEY_PRODUCTION_YEAR, production_year); |
| initialValues.put(KEY_COLUMN_DURATION, duration); |
| return mDatabase.insert(FTS_VIRTUAL_TABLE, null, initialValues); |
| } |
| |
| @Override |
| public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { |
| Log.w(TAG, "Upgrading database from version " + oldVersion + " to " |
| + newVersion + ", which will destroy all old data"); |
| db.execSQL("DROP TABLE IF EXISTS " + FTS_VIRTUAL_TABLE); |
| onCreate(db); |
| } |
| |
| |
| } |
| |
| } |