1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.tvleanback.data; 18 19 import android.app.SearchManager; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.database.sqlite.SQLiteDatabase; 24 import android.database.sqlite.SQLiteOpenHelper; 25 import android.database.sqlite.SQLiteQueryBuilder; 26 import android.media.Rating; 27 import android.provider.BaseColumns; 28 import android.util.Log; 29 30 import com.example.android.tvleanback.R; 31 import com.example.android.tvleanback.model.Movie; 32 33 import org.json.JSONException; 34 35 import java.io.IOException; 36 import java.util.HashMap; 37 import java.util.List; 38 import java.util.Map; 39 40 /** 41 * Contains logic to return specific words from the video database, and 42 * load the video database table when it needs to be created. 43 */ 44 public class VideoDatabase { 45 //The columns we'll include in the video database table 46 public static final String KEY_NAME = SearchManager.SUGGEST_COLUMN_TEXT_1; 47 public static final String KEY_DESCRIPTION = SearchManager.SUGGEST_COLUMN_TEXT_2; 48 public static final String KEY_ICON = SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE; 49 public static final String KEY_DATA_TYPE = SearchManager.SUGGEST_COLUMN_CONTENT_TYPE; 50 public static final String KEY_IS_LIVE = SearchManager.SUGGEST_COLUMN_IS_LIVE; 51 public static final String KEY_VIDEO_WIDTH = SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH; 52 public static final String KEY_VIDEO_HEIGHT = SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT; 53 public static final String KEY_AUDIO_CHANNEL_CONFIG = 54 SearchManager.SUGGEST_COLUMN_AUDIO_CHANNEL_CONFIG; 55 public static final String KEY_PURCHASE_PRICE = SearchManager.SUGGEST_COLUMN_PURCHASE_PRICE; 56 public static final String KEY_RENTAL_PRICE = SearchManager.SUGGEST_COLUMN_RENTAL_PRICE; 57 public static final String KEY_RATING_STYLE = SearchManager.SUGGEST_COLUMN_RATING_STYLE; 58 public static final String KEY_RATING_SCORE = SearchManager.SUGGEST_COLUMN_RATING_SCORE; 59 public static final String KEY_PRODUCTION_YEAR = SearchManager.SUGGEST_COLUMN_PRODUCTION_YEAR; 60 public static final String KEY_COLUMN_DURATION = SearchManager.SUGGEST_COLUMN_DURATION; 61 public static final String KEY_ACTION = SearchManager.SUGGEST_COLUMN_INTENT_ACTION; 62 private static final String TAG = "VideoDatabase"; 63 private static final String DATABASE_NAME = "video_database_leanback"; 64 private static final String FTS_VIRTUAL_TABLE = "Leanback_table"; 65 private static final int DATABASE_VERSION = 2; 66 private static final HashMap<String, String> COLUMN_MAP = buildColumnMap(); 67 private static int CARD_WIDTH = 313; 68 private static int CARD_HEIGHT = 176; 69 private final VideoDatabaseOpenHelper mDatabaseOpenHelper; 70 71 /** 72 * Constructor 73 * 74 * @param context The Context within which to work, used to create the DB 75 */ 76 public VideoDatabase(Context context) { 77 mDatabaseOpenHelper = new VideoDatabaseOpenHelper(context); 78 } 79 80 /** 81 * Builds a map for all columns that may be requested, which will be given to the 82 * SQLiteQueryBuilder. This is a good way to define aliases for column names, but must include 83 * all columns, even if the value is the key. This allows the ContentProvider to request 84 * columns w/o the need to know real column names and create the alias itself. 85 */ 86 private static HashMap<String, String> buildColumnMap() { 87 HashMap<String, String> map = new HashMap<String, String>(); 88 map.put(KEY_NAME, KEY_NAME); 89 map.put(KEY_DESCRIPTION, KEY_DESCRIPTION); 90 map.put(KEY_ICON, KEY_ICON); 91 map.put(KEY_DATA_TYPE, KEY_DATA_TYPE); 92 map.put(KEY_IS_LIVE, KEY_IS_LIVE); 93 map.put(KEY_VIDEO_WIDTH, KEY_VIDEO_WIDTH); 94 map.put(KEY_VIDEO_HEIGHT, KEY_VIDEO_HEIGHT); 95 map.put(KEY_AUDIO_CHANNEL_CONFIG, KEY_AUDIO_CHANNEL_CONFIG); 96 map.put(KEY_PURCHASE_PRICE, KEY_PURCHASE_PRICE); 97 map.put(KEY_RENTAL_PRICE, KEY_RENTAL_PRICE); 98 map.put(KEY_RATING_STYLE, KEY_RATING_STYLE); 99 map.put(KEY_RATING_SCORE, KEY_RATING_SCORE); 100 map.put(KEY_PRODUCTION_YEAR, KEY_PRODUCTION_YEAR); 101 map.put(KEY_COLUMN_DURATION, KEY_COLUMN_DURATION); 102 map.put(KEY_ACTION, KEY_ACTION); 103 map.put(BaseColumns._ID, "rowid AS " + 104 BaseColumns._ID); 105 map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, "rowid AS " + 106 SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID); 107 map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, "rowid AS " + 108 SearchManager.SUGGEST_COLUMN_SHORTCUT_ID); 109 return map; 110 } 111 112 /** 113 * Returns a Cursor positioned at the word specified by rowId 114 * 115 * @param rowId id of word to retrieve 116 * @param columns The columns to include, if null then all are included 117 * @return Cursor positioned to matching word, or null if not found. 118 */ 119 public Cursor getWord(String rowId, String[] columns) { 120 /* This builds a query that looks like: 121 * SELECT <columns> FROM <table> WHERE rowid = <rowId> 122 */ 123 String selection = "rowid = ?"; 124 String[] selectionArgs = new String[]{rowId}; 125 126 return query(selection, selectionArgs, columns); 127 } 128 129 /** 130 * Returns a Cursor over all words that match the first letter of the given query 131 * 132 * @param query The string to search for 133 * @param columns The columns to include, if null then all are included 134 * @return Cursor over all words that match, or null if none found. 135 */ 136 public Cursor getWordMatch(String query, String[] columns) { 137 /* This builds a query that looks like: 138 * SELECT <columns> FROM <table> WHERE <KEY_WORD> MATCH 'query*' 139 * which is an FTS3 search for the query text (plus a wildcard) inside the word column. 140 * 141 * - "rowid" is the unique id for all rows but we need this value for the "_id" column in 142 * order for the Adapters to work, so the columns need to make "_id" an alias for "rowid" 143 * - "rowid" also needs to be used by the SUGGEST_COLUMN_INTENT_DATA alias in order 144 * for suggestions to carry the proper intent data.SearchManager 145 * These aliases are defined in the VideoProvider when queries are made. 146 * - This can be revised to also search the definition text with FTS3 by changing 147 * the selection clause to use FTS_VIRTUAL_TABLE instead of KEY_WORD (to search across 148 * the entire table, but sorting the relevance could be difficult. 149 */ 150 String selection = KEY_NAME + " MATCH ?"; 151 String[] selectionArgs = new String[]{query + "*"}; 152 153 return query(selection, selectionArgs, columns); 154 } 155 156 /** 157 * Performs a database query. 158 * 159 * @param selection The selection clause 160 * @param selectionArgs Selection arguments for "?" components in the selection 161 * @param columns The columns to return 162 * @return A Cursor over all rows matching the query 163 */ 164 private Cursor query(String selection, String[] selectionArgs, String[] columns) { 165 /* The SQLiteBuilder provides a map for all possible columns requested to 166 * actual columns in the database, creating a simple column alias mechanism 167 * by which the ContentProvider does not need to know the real column names 168 */ 169 SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); 170 builder.setTables(FTS_VIRTUAL_TABLE); 171 builder.setProjectionMap(COLUMN_MAP); 172 173 Cursor cursor = new PaginatedCursor(builder.query(mDatabaseOpenHelper.getReadableDatabase(), 174 columns, selection, selectionArgs, null, null, null)); 175 176 if (cursor == null) { 177 return null; 178 } else if (!cursor.moveToFirst()) { 179 cursor.close(); 180 return null; 181 } 182 return cursor; 183 } 184 185 /** 186 * This creates/opens the database. 187 */ 188 private static class VideoDatabaseOpenHelper extends SQLiteOpenHelper { 189 190 private final Context mHelperContext; 191 private SQLiteDatabase mDatabase; 192 193 VideoDatabaseOpenHelper(Context context) { 194 super(context, DATABASE_NAME, null, DATABASE_VERSION); 195 mHelperContext = context; 196 } 197 198 /* Note that FTS3 does not support column constraints and thus, you cannot 199 * declare a primary key. However, "rowid" is automatically used as a unique 200 * identifier, so when making requests, we will use "_id" as an alias for "rowid" 201 */ 202 private static final String FTS_TABLE_CREATE = 203 "CREATE VIRTUAL TABLE " + FTS_VIRTUAL_TABLE + 204 " USING fts3 (" + 205 KEY_NAME + ", " + 206 KEY_DESCRIPTION + "," + 207 KEY_ICON + "," + 208 KEY_DATA_TYPE + "," + 209 KEY_IS_LIVE + "," + 210 KEY_VIDEO_WIDTH + "," + 211 KEY_VIDEO_HEIGHT + "," + 212 KEY_AUDIO_CHANNEL_CONFIG + "," + 213 KEY_PURCHASE_PRICE + "," + 214 KEY_RENTAL_PRICE + "," + 215 KEY_RATING_STYLE + "," + 216 KEY_RATING_SCORE + "," + 217 KEY_PRODUCTION_YEAR + "," + 218 KEY_COLUMN_DURATION + "," + 219 KEY_ACTION + ");"; 220 221 @Override 222 public void onCreate(SQLiteDatabase db) { 223 mDatabase = db; 224 mDatabase.execSQL(FTS_TABLE_CREATE); 225 loadDatabase(); 226 } 227 228 /** 229 * Starts a thread to load the database table with words 230 */ 231 private void loadDatabase() { 232 new Thread(new Runnable() { 233 public void run() { 234 try { 235 loadMovies(); 236 } catch (IOException e) { 237 throw new RuntimeException(e); 238 } 239 } 240 }).start(); 241 } 242 243 private void loadMovies() throws IOException { 244 Log.d(TAG, "Loading movies..."); 245 246 HashMap<String, List<Movie>> movies = null; 247 try { 248 VideoProvider.setContext(mHelperContext); 249 movies = VideoProvider.buildMedia(mHelperContext, 250 mHelperContext.getResources().getString(R.string.catalog_url)); 251 } catch (JSONException e) { 252 Log.e(TAG, "JSon Exception when loading movie", e); 253 } 254 255 for (Map.Entry<String, List<Movie>> entry : movies.entrySet()) { 256 List<Movie> list = entry.getValue(); 257 for (Movie movie : list) { 258 long id = addMovie(movie); 259 if (id < 0) { 260 Log.e(TAG, "unable to add movie: " + movie.toString()); 261 } 262 } 263 } 264 // add dummy movies to illustrate action deep link in search detail 265 // Android TV Search requires that the medias title, MIME type, production year, 266 // and duration all match exactly to those found from Googles servers. 267 addMovieForDeepLink(mHelperContext.getString(R.string.noah_title), 268 mHelperContext.getString(R.string.noah_description), 269 R.drawable.noah, 270 8280000, 271 "2014"); 272 addMovieForDeepLink(mHelperContext.getString(R.string.dragon2_title), 273 mHelperContext.getString(R.string.dragon2_description), 274 R.drawable.dragon2, 275 6300000, 276 "2014"); 277 addMovieForDeepLink(mHelperContext.getString(R.string.maleficent_title), 278 mHelperContext.getString(R.string.maleficent_description), 279 R.drawable.maleficent, 280 5820000, 281 "2014"); 282 } 283 284 /** 285 * Add a movie to the database. 286 * 287 * @return rowId or -1 if failed 288 */ 289 public long addMovie(Movie movie) { 290 ContentValues initialValues = new ContentValues(); 291 initialValues.put(KEY_NAME, movie.getTitle()); 292 initialValues.put(KEY_DESCRIPTION, movie.getDescription()); 293 initialValues.put(KEY_ICON, movie.getCardImageUrl()); 294 initialValues.put(KEY_DATA_TYPE, "video/mp4"); 295 initialValues.put(KEY_IS_LIVE, false); 296 initialValues.put(KEY_VIDEO_WIDTH, CARD_WIDTH); 297 initialValues.put(KEY_VIDEO_HEIGHT, CARD_HEIGHT); 298 initialValues.put(KEY_AUDIO_CHANNEL_CONFIG, "2.0"); 299 initialValues.put(KEY_PURCHASE_PRICE, mHelperContext.getString(R.string.buy_2)); 300 initialValues.put(KEY_RENTAL_PRICE, mHelperContext.getString(R.string.rent_2)); 301 initialValues.put(KEY_RATING_STYLE, Rating.RATING_5_STARS); 302 initialValues.put(KEY_RATING_SCORE, 3.5f); 303 initialValues.put(KEY_PRODUCTION_YEAR, 2014); 304 initialValues.put(KEY_COLUMN_DURATION, 0); 305 initialValues.put(KEY_ACTION, mHelperContext.getString(R.string.global_search)); 306 return mDatabase.insert(FTS_VIRTUAL_TABLE, null, initialValues); 307 } 308 309 /** 310 * Add an entry to the database for dummy deep link. 311 * 312 * @return rowId or -1 if failed 313 */ 314 public long addMovieForDeepLink(String title, String description, int icon, long duration, String production_year) { 315 ContentValues initialValues = new ContentValues(); 316 initialValues.put(KEY_NAME, title); 317 initialValues.put(KEY_DESCRIPTION, description); 318 initialValues.put(KEY_ICON, icon); 319 initialValues.put(KEY_DATA_TYPE, "video/mp4"); 320 initialValues.put(KEY_IS_LIVE, false); 321 initialValues.put(KEY_VIDEO_WIDTH, 1280); 322 initialValues.put(KEY_VIDEO_HEIGHT, 720); 323 initialValues.put(KEY_AUDIO_CHANNEL_CONFIG, "2.0"); 324 initialValues.put(KEY_PURCHASE_PRICE, "Free"); 325 initialValues.put(KEY_RENTAL_PRICE, "Free"); 326 initialValues.put(KEY_RATING_STYLE, Rating.RATING_5_STARS); 327 initialValues.put(KEY_RATING_SCORE, 3.5f); 328 initialValues.put(KEY_PRODUCTION_YEAR, production_year); 329 initialValues.put(KEY_COLUMN_DURATION, duration); 330 return mDatabase.insert(FTS_VIRTUAL_TABLE, null, initialValues); 331 } 332 333 @Override 334 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 335 Log.w(TAG, "Upgrading database from version " + oldVersion + " to " 336 + newVersion + ", which will destroy all old data"); 337 db.execSQL("DROP TABLE IF EXISTS " + FTS_VIRTUAL_TABLE); 338 onCreate(db); 339 } 340 341 342 } 343 344 } 345