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.android.tv.dvr.provider;
18 
19 import android.content.ContentValues;
20 import android.content.Context;
21 import android.database.Cursor;
22 import android.database.sqlite.SQLiteDatabase;
23 import android.database.sqlite.SQLiteOpenHelper;
24 import android.database.sqlite.SQLiteQueryBuilder;
25 import android.database.sqlite.SQLiteStatement;
26 import android.provider.BaseColumns;
27 import android.support.annotation.VisibleForTesting;
28 import android.text.TextUtils;
29 import android.util.Log;
30 
31 import com.android.tv.common.dagger.annotations.ApplicationContext;
32 import com.android.tv.common.flags.DvrFlags;
33 import com.android.tv.dvr.data.ScheduledRecording;
34 import com.android.tv.dvr.data.SeriesRecording;
35 import com.android.tv.dvr.provider.DvrContract.Schedules;
36 import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
37 
38 import com.google.common.collect.ObjectArrays;
39 
40 import javax.inject.Inject;
41 import javax.inject.Singleton;
42 
43 /** A data class for one recorded contents. */
44 @Singleton
45 public class DvrDatabaseHelper extends SQLiteOpenHelper {
46     private static final String TAG = "DvrDatabaseHelper";
47     private static final boolean DEBUG = false;
48 
49     private static final int DATABASE_VERSION = 18;
50     private static final String DB_NAME = "dvr.db";
51     private static final String NOT_NULL = " NOT NULL";
52     private static final String PRIMARY_KEY_AUTOINCREMENT = " PRIMARY KEY AUTOINCREMENT";
53 
54     private static final int SQL_DATA_TYPE_LONG = 0;
55     private static final int SQL_DATA_TYPE_INT = 1;
56     private static final int SQL_DATA_TYPE_STRING = 2;
57 
58     private static final ColumnInfo[] COLUMNS_SCHEDULES =
59             new ColumnInfo[] {
60                 new ColumnInfo(Schedules._ID, SQL_DATA_TYPE_LONG, PRIMARY_KEY_AUTOINCREMENT),
61                 new ColumnInfo(
62                         Schedules.COLUMN_PRIORITY,
63                         SQL_DATA_TYPE_LONG,
64                         defaultConstraint(ScheduledRecording.DEFAULT_PRIORITY)),
65                 new ColumnInfo(Schedules.COLUMN_TYPE, SQL_DATA_TYPE_STRING, NOT_NULL),
66                 new ColumnInfo(Schedules.COLUMN_INPUT_ID, SQL_DATA_TYPE_STRING, NOT_NULL),
67                 new ColumnInfo(Schedules.COLUMN_CHANNEL_ID, SQL_DATA_TYPE_LONG, NOT_NULL),
68                 new ColumnInfo(Schedules.COLUMN_PROGRAM_ID, SQL_DATA_TYPE_LONG),
69                 new ColumnInfo(Schedules.COLUMN_PROGRAM_TITLE, SQL_DATA_TYPE_STRING),
70                 new ColumnInfo(
71                         Schedules.COLUMN_START_TIME_UTC_MILLIS,
72                         SQL_DATA_TYPE_LONG,
73                         NOT_NULL),
74                 new ColumnInfo(
75                         Schedules.COLUMN_END_TIME_UTC_MILLIS,
76                         SQL_DATA_TYPE_LONG,
77                         NOT_NULL),
78                 new ColumnInfo(Schedules.COLUMN_SEASON_NUMBER, SQL_DATA_TYPE_STRING),
79                 new ColumnInfo(Schedules.COLUMN_EPISODE_NUMBER, SQL_DATA_TYPE_STRING),
80                 new ColumnInfo(Schedules.COLUMN_EPISODE_TITLE, SQL_DATA_TYPE_STRING),
81                 new ColumnInfo(Schedules.COLUMN_PROGRAM_DESCRIPTION, SQL_DATA_TYPE_STRING),
82                 new ColumnInfo(Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, SQL_DATA_TYPE_STRING),
83                 new ColumnInfo(Schedules.COLUMN_PROGRAM_POST_ART_URI, SQL_DATA_TYPE_STRING),
84                 new ColumnInfo(Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, SQL_DATA_TYPE_STRING),
85                 new ColumnInfo(Schedules.COLUMN_STATE, SQL_DATA_TYPE_STRING, NOT_NULL),
86                 new ColumnInfo(
87                         Schedules.COLUMN_FAILED_REASON,
88                         SQL_DATA_TYPE_STRING),
89                 new ColumnInfo(
90                         Schedules.COLUMN_SERIES_RECORDING_ID,
91                         SQL_DATA_TYPE_LONG)
92             };
93 
94     private static final ColumnInfo[] COLUMNS_TIME_OFFSET =
95             new ColumnInfo[] {
96                 new ColumnInfo(
97                         Schedules.COLUMN_START_OFFSET_MILLIS,
98                         SQL_DATA_TYPE_LONG,
99                         defaultConstraint(ScheduledRecording.DEFAULT_TIME_OFFSET)),
100                 new ColumnInfo(
101                         Schedules.COLUMN_END_OFFSET_MILLIS,
102                         SQL_DATA_TYPE_LONG,
103                         defaultConstraint(ScheduledRecording.DEFAULT_TIME_OFFSET))
104             };
105 
106     private static final ColumnInfo[] COLUMNS_SCHEDULES_WITH_TIME_OFFSET =
107             ObjectArrays.concat(COLUMNS_SCHEDULES, COLUMNS_TIME_OFFSET, ColumnInfo.class);
108 
109     @VisibleForTesting
110     static final String SQL_CREATE_SCHEDULES =
111             buildCreateSchedulesSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES);
112     private static final String SQL_INSERT_SCHEDULES =
113             buildInsertSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES);
114     private static final String SQL_UPDATE_SCHEDULES =
115             buildUpdateSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES);
116 
117     private static final String SQL_CREATE_SCHEDULES_WITH_TIME_OFFSET =
118             buildCreateSchedulesSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES_WITH_TIME_OFFSET);
119     private static final String SQL_INSERT_SCHEDULES_WITH_TIME_OFFSET =
120             buildInsertSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES_WITH_TIME_OFFSET);
121     private static final String SQL_UPDATE_SCHEDULES_WITH_TIME_OFFSET =
122             buildUpdateSql(Schedules.TABLE_NAME, COLUMNS_SCHEDULES_WITH_TIME_OFFSET);
123 
124     private static final String SQL_DELETE_SCHEDULES = buildDeleteSql(Schedules.TABLE_NAME);
125     @VisibleForTesting
126     static final String SQL_DROP_SCHEDULES = buildDropSql(Schedules.TABLE_NAME);
127 
128     private static final ColumnInfo[] COLUMNS_SERIES_RECORDINGS =
129             new ColumnInfo[] {
130                 new ColumnInfo(SeriesRecordings._ID, SQL_DATA_TYPE_LONG, PRIMARY_KEY_AUTOINCREMENT),
131                 new ColumnInfo(
132                         SeriesRecordings.COLUMN_PRIORITY,
133                         SQL_DATA_TYPE_LONG,
134                         defaultConstraint(SeriesRecording.DEFAULT_PRIORITY)),
135                 new ColumnInfo(SeriesRecordings.COLUMN_TITLE, SQL_DATA_TYPE_STRING, NOT_NULL),
136                 new ColumnInfo(SeriesRecordings.COLUMN_SHORT_DESCRIPTION, SQL_DATA_TYPE_STRING),
137                 new ColumnInfo(SeriesRecordings.COLUMN_LONG_DESCRIPTION, SQL_DATA_TYPE_STRING),
138                 new ColumnInfo(SeriesRecordings.COLUMN_INPUT_ID, SQL_DATA_TYPE_STRING, NOT_NULL),
139                 new ColumnInfo(SeriesRecordings.COLUMN_CHANNEL_ID, SQL_DATA_TYPE_LONG, NOT_NULL),
140                 new ColumnInfo(SeriesRecordings.COLUMN_SERIES_ID, SQL_DATA_TYPE_STRING, NOT_NULL),
141                 new ColumnInfo(
142                         SeriesRecordings.COLUMN_START_FROM_SEASON,
143                         SQL_DATA_TYPE_INT,
144                         defaultConstraint(SeriesRecordings.THE_BEGINNING)),
145                 new ColumnInfo(
146                         SeriesRecordings.COLUMN_START_FROM_EPISODE,
147                         SQL_DATA_TYPE_INT,
148                         defaultConstraint(SeriesRecordings.THE_BEGINNING)),
149                 new ColumnInfo(
150                         SeriesRecordings.COLUMN_CHANNEL_OPTION,
151                         SQL_DATA_TYPE_STRING,
152                         defaultConstraint(SeriesRecordings.OPTION_CHANNEL_ONE)),
153                 new ColumnInfo(SeriesRecordings.COLUMN_CANONICAL_GENRE, SQL_DATA_TYPE_STRING),
154                 new ColumnInfo(SeriesRecordings.COLUMN_POSTER_URI, SQL_DATA_TYPE_STRING),
155                 new ColumnInfo(SeriesRecordings.COLUMN_PHOTO_URI, SQL_DATA_TYPE_STRING),
156                 new ColumnInfo(SeriesRecordings.COLUMN_STATE, SQL_DATA_TYPE_STRING)
157             };
158 
159     @VisibleForTesting
160     static final String SQL_CREATE_SERIES_RECORDINGS =
161             buildCreateSql(SeriesRecordings.TABLE_NAME, COLUMNS_SERIES_RECORDINGS, null);
162     private static final String SQL_INSERT_SERIES_RECORDINGS =
163             buildInsertSql(SeriesRecordings.TABLE_NAME, COLUMNS_SERIES_RECORDINGS);
164     private static final String SQL_UPDATE_SERIES_RECORDINGS =
165             buildUpdateSql(SeriesRecordings.TABLE_NAME, COLUMNS_SERIES_RECORDINGS);
166     private static final String SQL_DELETE_SERIES_RECORDINGS =
167             buildDeleteSql(SeriesRecordings.TABLE_NAME);
168     @VisibleForTesting
169     static final String SQL_DROP_SERIES_RECORDINGS =
170             buildDropSql(SeriesRecordings.TABLE_NAME);
171 
172     private final DvrFlags mDvrFlags;
173 
defaultConstraint(int value)174     private static String defaultConstraint(int value) {
175         return defaultConstraint(String.valueOf(value));
176     }
177 
defaultConstraint(long value)178     private static String defaultConstraint(long value) {
179         return defaultConstraint(String.valueOf(value));
180     }
181 
defaultConstraint(String value)182     private static String defaultConstraint(String value) {
183         return " DEFAULT " + value;
184     }
185 
foreignKeyConstraint( String column, String referenceTable, String referenceColumn)186     private static String foreignKeyConstraint(
187             String column,
188             String referenceTable,
189             String referenceColumn) {
190         return ",FOREIGN KEY(" + column + ") "
191                 + "REFERENCES " + referenceTable + "(" + referenceColumn + ") "
192                 + "ON UPDATE CASCADE ON DELETE SET NULL";
193     }
194 
buildCreateSchedulesSql(String tableName, ColumnInfo[] columns)195     private static String buildCreateSchedulesSql(String tableName, ColumnInfo[] columns) {
196         return buildCreateSql(
197                 tableName,
198                 columns,
199                 foreignKeyConstraint(
200                         Schedules.COLUMN_SERIES_RECORDING_ID,
201                         SeriesRecordings.TABLE_NAME,
202                         SeriesRecordings._ID));
203     }
204 
buildCreateSql( String tableName, ColumnInfo[] columns, String foreignKeyConstraint)205     private static String buildCreateSql(
206             String tableName,
207             ColumnInfo[] columns,
208             String foreignKeyConstraint) {
209         StringBuilder sb = new StringBuilder();
210         sb.append("CREATE TABLE ").append(tableName).append("(");
211         boolean appendComma = false;
212         for (ColumnInfo columnInfo : columns) {
213             if (appendComma) {
214                 sb.append(",");
215             }
216             appendComma = true;
217             sb.append(columnInfo.name);
218             switch (columnInfo.type) {
219                 case SQL_DATA_TYPE_LONG:
220                 case SQL_DATA_TYPE_INT:
221                     sb.append(" INTEGER");
222                     break;
223                 case SQL_DATA_TYPE_STRING:
224                     sb.append(" TEXT");
225                     break;
226             }
227             sb.append(columnInfo.constraint);
228         }
229         if (foreignKeyConstraint != null) {
230             sb.append(foreignKeyConstraint);
231         }
232         sb.append(");");
233         return sb.toString();
234     }
235 
buildSelectSql(ColumnInfo[] columns)236     private static String buildSelectSql(ColumnInfo[] columns) {
237         StringBuilder sb = new StringBuilder();
238         sb.append(" SELECT ");
239         boolean appendComma = false;
240         for (ColumnInfo columnInfo : columns) {
241             if (appendComma) {
242                 sb.append(",");
243             }
244             appendComma = true;
245             sb.append(columnInfo.name);
246         }
247         return sb.toString();
248     }
249 
buildInsertSql(String tableName, ColumnInfo[] columns)250     private static String buildInsertSql(String tableName, ColumnInfo[] columns) {
251         StringBuilder sb = new StringBuilder();
252         sb.append("INSERT INTO ").append(tableName).append(" (");
253         boolean appendComma = false;
254         for (ColumnInfo columnInfo : columns) {
255             if (appendComma) {
256                 sb.append(",");
257             }
258             appendComma = true;
259             sb.append(columnInfo.name);
260         }
261         sb.append(") VALUES (?");
262         for (int i = 1; i < columns.length; ++i) {
263             sb.append(",?");
264         }
265         sb.append(")");
266         return sb.toString();
267     }
268 
buildUpdateSql(String tableName, ColumnInfo[] columns)269     private static String buildUpdateSql(String tableName, ColumnInfo[] columns) {
270         StringBuilder sb = new StringBuilder();
271         sb.append("UPDATE ").append(tableName).append(" SET ");
272         boolean appendComma = false;
273         for (ColumnInfo columnInfo : columns) {
274             if (appendComma) {
275                 sb.append(",");
276             }
277             appendComma = true;
278             sb.append(columnInfo.name).append("=?");
279         }
280         sb.append(" WHERE ").append(BaseColumns._ID).append("=?");
281         return sb.toString();
282     }
283 
buildDeleteSql(String tableName)284     private static String buildDeleteSql(String tableName) {
285         return "DELETE FROM " + tableName + " WHERE " + BaseColumns._ID + "=?";
286     }
287 
buildDropSql(String tableName)288     private static String buildDropSql(String tableName) {
289         return "DROP TABLE IF EXISTS " + tableName;
290     }
291 
292     @Inject
DvrDatabaseHelper(@pplicationContext Context context, DvrFlags dvrFlags)293     public DvrDatabaseHelper(@ApplicationContext Context context, DvrFlags dvrFlags) {
294         super(context,
295                 DB_NAME,
296                 null,
297                 (dvrFlags.startEarlyEndLateEnabled() ? DATABASE_VERSION + 1 : DATABASE_VERSION));
298         mDvrFlags = dvrFlags;
299     }
300 
301     @Override
onConfigure(SQLiteDatabase db)302     public void onConfigure(SQLiteDatabase db) {
303         db.setForeignKeyConstraintsEnabled(true);
304     }
305 
306     @Override
onCreate(SQLiteDatabase db)307     public void onCreate(SQLiteDatabase db) {
308         if (mDvrFlags.startEarlyEndLateEnabled()) {
309             if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_SCHEDULES_WITH_TIME_OFFSET);
310             db.execSQL(SQL_CREATE_SCHEDULES_WITH_TIME_OFFSET);
311         } else {
312             if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_SCHEDULES);
313             db.execSQL(SQL_CREATE_SCHEDULES);
314         }
315         if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_CREATE_SERIES_RECORDINGS);
316         db.execSQL(SQL_CREATE_SERIES_RECORDINGS);
317     }
318 
319     @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)320     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
321         if (oldVersion < 17) {
322             if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SCHEDULES);
323             db.execSQL(SQL_DROP_SCHEDULES);
324             if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SERIES_RECORDINGS);
325             db.execSQL(SQL_DROP_SERIES_RECORDINGS);
326             onCreate(db);
327             return;
328         }
329         if (oldVersion < 18) {
330             db.execSQL(
331                     "ALTER TABLE "
332                             + Schedules.TABLE_NAME
333                             + " ADD COLUMN "
334                             + Schedules.COLUMN_FAILED_REASON
335                             + " TEXT DEFAULT null;");
336         }
337         if (mDvrFlags.startEarlyEndLateEnabled() && oldVersion < 19) {
338             db.execSQL("ALTER TABLE " + Schedules.TABLE_NAME + " ADD COLUMN "
339                     + Schedules.COLUMN_START_OFFSET_MILLIS + " INTEGER NOT NULL DEFAULT '0';");
340             db.execSQL("ALTER TABLE " + Schedules.TABLE_NAME + " ADD COLUMN "
341                     + Schedules.COLUMN_END_OFFSET_MILLIS + " INTEGER NOT NULL DEFAULT '0';");
342         }
343     }
344 
345     @Override
onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)346     public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
347         if (oldVersion > DATABASE_VERSION) {
348             String schedulesBackup = "schedules_backup";
349             db.execSQL(buildCreateSchedulesSql(schedulesBackup, COLUMNS_SCHEDULES));
350             db.execSQL("INSERT INTO " + schedulesBackup +
351                     buildSelectSql(COLUMNS_SCHEDULES) + " FROM " + Schedules.TABLE_NAME);
352             db.execSQL(SQL_DROP_SCHEDULES);
353             db.execSQL(SQL_CREATE_SCHEDULES);
354             db.execSQL("INSERT INTO " + Schedules.TABLE_NAME +
355                     buildSelectSql(COLUMNS_SCHEDULES) + " FROM " + schedulesBackup);
356             db.execSQL(buildDropSql(schedulesBackup));
357         }
358     }
359 
360     /** Handles the query request and returns a {@link Cursor}. */
query(String tableName, String[] projections)361     public Cursor query(String tableName, String[] projections) {
362         SQLiteDatabase db = getReadableDatabase();
363         SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
364         builder.setTables(tableName);
365         return builder.query(db, projections, null, null, null, null, null);
366     }
367 
368     /** Inserts schedules. */
insertSchedules(ScheduledRecording... scheduledRecordings)369     public void insertSchedules(ScheduledRecording... scheduledRecordings) {
370         SQLiteDatabase db = getWritableDatabase();
371         db.beginTransaction();
372         try {
373             if (mDvrFlags.startEarlyEndLateEnabled()) {
374                 SQLiteStatement statement =
375                         db.compileStatement(SQL_INSERT_SCHEDULES_WITH_TIME_OFFSET);
376                 for (ScheduledRecording r : scheduledRecordings) {
377                     statement.clearBindings();
378                     ContentValues values = ScheduledRecording.toContentValuesWithTimeOffset(r);
379                     bindColumns(statement, COLUMNS_SCHEDULES_WITH_TIME_OFFSET, values);
380                     statement.execute();
381                 }
382             } else {
383                 SQLiteStatement statement = db.compileStatement(SQL_INSERT_SCHEDULES);
384                 for (ScheduledRecording r : scheduledRecordings) {
385                     statement.clearBindings();
386                     ContentValues values = ScheduledRecording.toContentValues(r);
387                     bindColumns(statement, COLUMNS_SCHEDULES, values);
388                     statement.execute();
389                 }
390             }
391             db.setTransactionSuccessful();
392         } finally {
393             db.endTransaction();
394         }
395     }
396 
397     /** Update schedules. */
updateSchedules(ScheduledRecording... scheduledRecordings)398     public void updateSchedules(ScheduledRecording... scheduledRecordings) {
399         SQLiteDatabase db = getWritableDatabase();
400         db.beginTransaction();
401         try {
402             if (mDvrFlags.startEarlyEndLateEnabled()) {
403                 SQLiteStatement statement =
404                         db.compileStatement(SQL_UPDATE_SCHEDULES_WITH_TIME_OFFSET);
405                 for (ScheduledRecording r : scheduledRecordings) {
406                     statement.clearBindings();
407                     ContentValues values = ScheduledRecording.toContentValuesWithTimeOffset(r);
408                     bindColumns(statement, COLUMNS_SCHEDULES_WITH_TIME_OFFSET, values);
409                     statement.bindLong(COLUMNS_SCHEDULES_WITH_TIME_OFFSET.length + 1, r.getId());
410                     statement.execute();
411                 }
412             } else {
413                 SQLiteStatement statement = db.compileStatement(SQL_UPDATE_SCHEDULES);
414                 for (ScheduledRecording r : scheduledRecordings) {
415                     statement.clearBindings();
416                     ContentValues values = ScheduledRecording.toContentValues(r);
417                     bindColumns(statement, COLUMNS_SCHEDULES, values);
418                     statement.bindLong(COLUMNS_SCHEDULES.length + 1, r.getId());
419                     statement.execute();
420                 }
421             }
422             db.setTransactionSuccessful();
423         } finally {
424             db.endTransaction();
425         }
426     }
427 
428     /** Delete schedules. */
deleteSchedules(ScheduledRecording... scheduledRecordings)429     public void deleteSchedules(ScheduledRecording... scheduledRecordings) {
430         SQLiteDatabase db = getWritableDatabase();
431         SQLiteStatement statement = db.compileStatement(SQL_DELETE_SCHEDULES);
432         db.beginTransaction();
433         try {
434             for (ScheduledRecording r : scheduledRecordings) {
435                 statement.clearBindings();
436                 statement.bindLong(1, r.getId());
437                 statement.execute();
438             }
439             db.setTransactionSuccessful();
440         } finally {
441             db.endTransaction();
442         }
443     }
444 
445     /** Inserts series recordings. */
insertSeriesRecordings(SeriesRecording... seriesRecordings)446     public void insertSeriesRecordings(SeriesRecording... seriesRecordings) {
447         SQLiteDatabase db = getWritableDatabase();
448         SQLiteStatement statement = db.compileStatement(SQL_INSERT_SERIES_RECORDINGS);
449         db.beginTransaction();
450         try {
451             for (SeriesRecording r : seriesRecordings) {
452                 statement.clearBindings();
453                 ContentValues values = SeriesRecording.toContentValues(r);
454                 bindColumns(statement, COLUMNS_SERIES_RECORDINGS, values);
455                 statement.execute();
456             }
457             db.setTransactionSuccessful();
458         } finally {
459             db.endTransaction();
460         }
461     }
462 
463     /** Update series recordings. */
updateSeriesRecordings(SeriesRecording... seriesRecordings)464     public void updateSeriesRecordings(SeriesRecording... seriesRecordings) {
465         SQLiteDatabase db = getWritableDatabase();
466         SQLiteStatement statement = db.compileStatement(SQL_UPDATE_SERIES_RECORDINGS);
467         db.beginTransaction();
468         try {
469             for (SeriesRecording r : seriesRecordings) {
470                 statement.clearBindings();
471                 ContentValues values = SeriesRecording.toContentValues(r);
472                 bindColumns(statement, COLUMNS_SERIES_RECORDINGS, values);
473                 statement.bindLong(COLUMNS_SERIES_RECORDINGS.length + 1, r.getId());
474                 statement.execute();
475             }
476             db.setTransactionSuccessful();
477         } finally {
478             db.endTransaction();
479         }
480     }
481 
482     /** Delete series recordings. */
deleteSeriesRecordings(SeriesRecording... seriesRecordings)483     public void deleteSeriesRecordings(SeriesRecording... seriesRecordings) {
484         SQLiteDatabase db = getWritableDatabase();
485         SQLiteStatement statement = db.compileStatement(SQL_DELETE_SERIES_RECORDINGS);
486         db.beginTransaction();
487         try {
488             for (SeriesRecording r : seriesRecordings) {
489                 statement.clearBindings();
490                 statement.bindLong(1, r.getId());
491                 statement.execute();
492             }
493             db.setTransactionSuccessful();
494         } finally {
495             db.endTransaction();
496         }
497     }
498 
bindColumns( SQLiteStatement statement, ColumnInfo[] columns, ContentValues values)499     private void bindColumns(
500             SQLiteStatement statement, ColumnInfo[] columns, ContentValues values) {
501         for (int i = 0; i < columns.length; ++i) {
502             ColumnInfo columnInfo = columns[i];
503             Object value = values.get(columnInfo.name);
504             switch (columnInfo.type) {
505                 case SQL_DATA_TYPE_LONG:
506                     if (value == null) {
507                         statement.bindNull(i + 1);
508                     } else {
509                         statement.bindLong(i + 1, (Long) value);
510                     }
511                     break;
512                 case SQL_DATA_TYPE_INT:
513                     if (value == null) {
514                         statement.bindNull(i + 1);
515                     } else {
516                         statement.bindLong(i + 1, (Integer) value);
517                     }
518                     break;
519                 case SQL_DATA_TYPE_STRING:
520                     {
521                         if (TextUtils.isEmpty((String) value)) {
522                             statement.bindNull(i + 1);
523                         } else {
524                             statement.bindString(i + 1, (String) value);
525                         }
526                         break;
527                     }
528             }
529         }
530     }
531 
532     private static class ColumnInfo {
533         final String name;
534         final int type;
535         final String constraint;
536 
ColumnInfo(String name, int type)537         ColumnInfo(String name, int type) {
538             this(name, type, "");
539         }
540 
ColumnInfo(String name, int type, String constraint)541         ColumnInfo(String name, int type, String constraint) {
542             this.name = name;
543             this.type = type;
544             this.constraint = constraint;
545         }
546     }
547 }
548