TvProvider.java revision 711f02f31b2be633a19fc929761581116cb0c64b
1/*
2 * Copyright (C) 2014 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
17package com.android.providers.tv;
18
19import android.content.ComponentName;
20import android.content.ContentProvider;
21import android.content.ContentProviderOperation;
22import android.content.ContentProviderResult;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.OperationApplicationException;
26import android.content.UriMatcher;
27import android.content.pm.PackageManager;
28import android.database.Cursor;
29import android.database.DatabaseUtils;
30import android.database.SQLException;
31import android.database.sqlite.SQLiteDatabase;
32import android.database.sqlite.SQLiteOpenHelper;
33import android.database.sqlite.SQLiteQueryBuilder;
34import android.graphics.Bitmap;
35import android.graphics.BitmapFactory;
36import android.media.tv.TvContract;
37import android.media.tv.TvContract.BaseTvColumns;
38import android.media.tv.TvContract.Channels;
39import android.media.tv.TvContract.Programs;
40import android.media.tv.TvContract.WatchedPrograms;
41import android.net.Uri;
42import android.os.AsyncTask;
43import android.os.ParcelFileDescriptor;
44import android.os.ParcelFileDescriptor.AutoCloseInputStream;
45import android.text.TextUtils;
46import android.util.Log;
47
48import com.google.android.collect.Sets;
49
50import libcore.io.IoUtils;
51
52import java.io.ByteArrayOutputStream;
53import java.io.File;
54import java.io.FileNotFoundException;
55import java.io.IOException;
56import java.util.ArrayList;
57import java.util.HashMap;
58import java.util.Set;
59
60/**
61 * TV content provider. The contract between this provider and applications is defined in
62 * {@link android.media.tv.TvContract}.
63 */
64public class TvProvider extends ContentProvider {
65    // STOPSHIP: Turn debugging off.
66    private static final boolean DEBUG = true;
67    private static final String TAG = "TvProvider";
68
69    private static final UriMatcher sUriMatcher;
70    private static final int MATCH_CHANNEL = 1;
71    private static final int MATCH_CHANNEL_ID = 2;
72    private static final int MATCH_CHANNEL_ID_LOGO = 3;
73    private static final int MATCH_CHANNEL_ID_PROGRAM = 4;
74    private static final int MATCH_INPUT_PACKAGE_SERVICE_CHANNEL = 5;
75    private static final int MATCH_PROGRAM = 6;
76    private static final int MATCH_PROGRAM_ID = 7;
77    private static final int MATCH_WATCHED_PROGRAM = 8;
78    private static final int MATCH_WATCHED_PROGRAM_ID = 9;
79
80    private static final String SELECTION_OVERLAPPED_PROGRAM = Programs.COLUMN_CHANNEL_ID
81            + "=? AND " + Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND "
82            + Programs.COLUMN_END_TIME_UTC_MILLIS + ">=?";
83
84    private static final String SELECTION_CHANNEL_BY_INPUT = Channels.COLUMN_PACKAGE_NAME
85            + "=? AND " + Channels.COLUMN_SERVICE_NAME + "=?";
86
87    private static final String CHANNELS_COLUMN_LOGO = "logo";
88    private static final int MAX_LOGO_IMAGE_SIZE = 256;
89
90    // STOPSHIP: Put this into the contract class.
91    private static final String Programs_COLUMN_VIDEO_RESOLUTION = "video_resolution";
92
93    private static HashMap<String, String> sChannelProjectionMap;
94    private static HashMap<String, String> sProgramProjectionMap;
95    private static HashMap<String, String> sWatchedProgramProjectionMap;
96
97    static {
98        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
99        sUriMatcher.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL);
100        sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID);
101        sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#/logo", MATCH_CHANNEL_ID_LOGO);
102        sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#/program", MATCH_CHANNEL_ID_PROGRAM);
103        sUriMatcher.addURI(TvContract.AUTHORITY, "input/*/*/channel",
104                MATCH_INPUT_PACKAGE_SERVICE_CHANNEL);
105        sUriMatcher.addURI(TvContract.AUTHORITY, "program", MATCH_PROGRAM);
106        sUriMatcher.addURI(TvContract.AUTHORITY, "program/#", MATCH_PROGRAM_ID);
107        sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program", MATCH_WATCHED_PROGRAM);
108        sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID);
109
110        sChannelProjectionMap = new HashMap<String, String>();
111        sChannelProjectionMap.put(Channels._ID, Channels._ID);
112        sChannelProjectionMap.put(Channels.COLUMN_PACKAGE_NAME, Channels.COLUMN_PACKAGE_NAME);
113        sChannelProjectionMap.put(Channels.COLUMN_SERVICE_NAME, Channels.COLUMN_SERVICE_NAME);
114        sChannelProjectionMap.put(Channels.COLUMN_TYPE, Channels.COLUMN_TYPE);
115        sChannelProjectionMap.put(Channels.COLUMN_TRANSPORT_STREAM_ID,
116                Channels.COLUMN_TRANSPORT_STREAM_ID);
117        sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NUMBER, Channels.COLUMN_DISPLAY_NUMBER);
118        sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NAME, Channels.COLUMN_DISPLAY_NAME);
119        sChannelProjectionMap.put(Channels.COLUMN_DESCRIPTION, Channels.COLUMN_DESCRIPTION);
120        sChannelProjectionMap.put(Channels.COLUMN_BROWSABLE, Channels.COLUMN_BROWSABLE);
121        sChannelProjectionMap.put(Channels.COLUMN_SEARCHABLE, Channels.COLUMN_SEARCHABLE);
122        sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA,
123                Channels.COLUMN_INTERNAL_PROVIDER_DATA);
124        sChannelProjectionMap.put(Channels.COLUMN_VERSION_NUMBER, Channels.COLUMN_VERSION_NUMBER);
125
126        sProgramProjectionMap = new HashMap<String, String>();
127        sProgramProjectionMap.put(Programs._ID, Programs._ID);
128        sProgramProjectionMap.put(Programs.COLUMN_PACKAGE_NAME, Programs.COLUMN_PACKAGE_NAME);
129        sProgramProjectionMap.put(Programs.COLUMN_CHANNEL_ID, Programs.COLUMN_CHANNEL_ID);
130        sProgramProjectionMap.put(Programs.COLUMN_TITLE, Programs.COLUMN_TITLE);
131        sProgramProjectionMap.put(Programs.COLUMN_START_TIME_UTC_MILLIS,
132                Programs.COLUMN_START_TIME_UTC_MILLIS);
133        sProgramProjectionMap.put(Programs.COLUMN_END_TIME_UTC_MILLIS,
134                Programs.COLUMN_END_TIME_UTC_MILLIS);
135        sProgramProjectionMap.put(Programs.COLUMN_SHORT_DESCRIPTION,
136                Programs.COLUMN_SHORT_DESCRIPTION);
137        sProgramProjectionMap.put(Programs.COLUMN_LONG_DESCRIPTION,
138                Programs.COLUMN_LONG_DESCRIPTION);
139        sProgramProjectionMap.put(Programs_COLUMN_VIDEO_RESOLUTION,
140                Programs_COLUMN_VIDEO_RESOLUTION);
141        sProgramProjectionMap.put(Programs.COLUMN_POSTER_ART_URI,
142                Programs.COLUMN_POSTER_ART_URI);
143        sProgramProjectionMap.put(Programs.COLUMN_THUMBNAIL_URI,
144                Programs.COLUMN_THUMBNAIL_URI);
145        sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_DATA,
146                Programs.COLUMN_INTERNAL_PROVIDER_DATA);
147        sProgramProjectionMap.put(Programs.COLUMN_VERSION_NUMBER, Programs.COLUMN_VERSION_NUMBER);
148
149        sWatchedProgramProjectionMap = new HashMap<String, String>();
150        sWatchedProgramProjectionMap.put(WatchedPrograms._ID, WatchedPrograms._ID);
151        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
152                WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS);
153        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS,
154                WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS);
155        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_CHANNEL_ID,
156                WatchedPrograms.COLUMN_CHANNEL_ID);
157        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_TITLE,
158                WatchedPrograms.COLUMN_TITLE);
159        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS,
160                WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS);
161        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS,
162                WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS);
163        sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_DESCRIPTION,
164                WatchedPrograms.COLUMN_DESCRIPTION);
165    }
166
167    private static final int DATABASE_VERSION = 7;
168    private static final String DATABASE_NAME = "tv.db";
169    private static final String CHANNELS_TABLE = "channels";
170    private static final String PROGRAMS_TABLE = "programs";
171    private static final String WATCHED_PROGRAMS_TABLE = "watched_programs";
172    private static final String DEFAULT_CHANNELS_SORT_ORDER = Channels.COLUMN_DISPLAY_NUMBER
173            + " ASC";
174    private static final String DEFAULT_PROGRAMS_SORT_ORDER = Programs.COLUMN_START_TIME_UTC_MILLIS
175            + " ASC";
176    private static final String DEFAULT_WATCHED_PROGRAMS_SORT_ORDER =
177            WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC";
178
179    private static final String PERMISSION_ALL_EPG_DATA = "android.permission.ALL_EPG_DATA";
180
181    private static class DatabaseHelper extends SQLiteOpenHelper {
182        private Context mContext;
183
184        DatabaseHelper(Context context) {
185            super(context, DATABASE_NAME, null, DATABASE_VERSION);
186            mContext = context;
187        }
188
189        @Override
190        public void onConfigure(SQLiteDatabase db) {
191            db.setForeignKeyConstraintsEnabled(true);
192        }
193
194        @Override
195        public void onCreate(SQLiteDatabase db) {
196            if (DEBUG) {
197                Log.d(TAG, "Creating database");
198            }
199            // Set up the database schema.
200            db.execSQL("CREATE TABLE " + CHANNELS_TABLE + " ("
201                    + Channels._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
202                    + Channels.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
203                    + Channels.COLUMN_SERVICE_NAME + " TEXT NOT NULL,"
204                    + Channels.COLUMN_TYPE + " INTEGER NOT NULL DEFAULT 0,"
205                    + Channels.COLUMN_SERVICE_TYPE + " INTEGER NOT NULL DEFAULT 1,"
206                    + Channels.COLUMN_ORIGINAL_NETWORK_ID + " INTEGER,"
207                    + Channels.COLUMN_TRANSPORT_STREAM_ID + " INTEGER,"
208                    + Channels.COLUMN_SERVICE_ID + " INTEGER,"
209                    + Channels.COLUMN_DISPLAY_NUMBER + " TEXT,"
210                    + Channels.COLUMN_DISPLAY_NAME + " TEXT,"
211                    + Channels.COLUMN_DESCRIPTION + " TEXT,"
212                    + Channels.COLUMN_BROWSABLE + " INTEGER NOT NULL DEFAULT 1,"
213                    + Channels.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
214                    + Channels.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
215                    + CHANNELS_COLUMN_LOGO + " BLOB,"
216                    + Channels.COLUMN_VERSION_NUMBER + " INTEGER"
217                    + ");");
218            db.execSQL("CREATE TABLE " + PROGRAMS_TABLE + " ("
219                    + Programs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
220                    + Programs.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
221                    + Programs.COLUMN_CHANNEL_ID + " INTEGER,"
222                    + Programs.COLUMN_TITLE + " TEXT,"
223                    + Programs.COLUMN_START_TIME_UTC_MILLIS + " INTEGER,"
224                    + Programs.COLUMN_END_TIME_UTC_MILLIS + " INTEGER,"
225                    + Programs.COLUMN_SHORT_DESCRIPTION + " TEXT,"
226                    + Programs.COLUMN_LONG_DESCRIPTION + " TEXT,"
227                    + Programs_COLUMN_VIDEO_RESOLUTION + " TEXT,"
228                    + Programs.COLUMN_POSTER_ART_URI + " TEXT,"
229                    + Programs.COLUMN_THUMBNAIL_URI + " TEXT,"
230                    + Programs.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
231                    + Programs.COLUMN_VERSION_NUMBER + " INTEGER,"
232                    + "FOREIGN KEY(" + Programs.COLUMN_CHANNEL_ID + ") REFERENCES "
233                            + CHANNELS_TABLE + "(" + Channels._ID + ")"
234                    + " ON UPDATE CASCADE ON DELETE CASCADE"
235                    + ");");
236            db.execSQL("CREATE TABLE " + WATCHED_PROGRAMS_TABLE + " ("
237                    + WatchedPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
238                    + WatchedPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
239                    + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " INTEGER,"
240                    + WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS + " INTEGER,"
241                    + WatchedPrograms.COLUMN_CHANNEL_ID + " INTEGER,"
242                    + WatchedPrograms.COLUMN_TITLE + " TEXT,"
243                    + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS + " INTEGER,"
244                    + WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS + " INTEGER,"
245                    + WatchedPrograms.COLUMN_DESCRIPTION + " TEXT,"
246                    + "FOREIGN KEY(" + WatchedPrograms.COLUMN_CHANNEL_ID + ") REFERENCES "
247                            + CHANNELS_TABLE + "(" + Channels._ID + ")"
248                    + " ON UPDATE CASCADE ON DELETE CASCADE"
249                    + ");");
250        }
251
252        @Override
253        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
254            if (DEBUG) {
255                Log.d(TAG, "Upgrading database from " + oldVersion + " to " + newVersion);
256            }
257
258            // Default upgrade case.
259            db.execSQL("DROP TABLE IF EXISTS " + WATCHED_PROGRAMS_TABLE);
260            db.execSQL("DROP TABLE IF EXISTS " + PROGRAMS_TABLE);
261            db.execSQL("DROP TABLE IF EXISTS " + CHANNELS_TABLE);
262
263            // Clear legacy logo directory
264            File logoPath = new File(mContext.getFilesDir(), "logo");
265            if (logoPath.exists()) {
266                for (File file : logoPath.listFiles()) {
267                    file.delete();
268                }
269                logoPath.delete();
270            }
271
272            onCreate(db);
273        }
274    }
275
276    private DatabaseHelper mOpenHelper;
277
278    @Override
279    public boolean onCreate() {
280        if (DEBUG) {
281          Log.d(TAG, "Creating TvProvider");
282        }
283        mOpenHelper = new DatabaseHelper(getContext());
284        return true;
285    }
286
287    @Override
288    public String getType(Uri uri) {
289        switch (sUriMatcher.match(uri)) {
290            case MATCH_CHANNEL:
291                return Channels.CONTENT_TYPE;
292            case MATCH_CHANNEL_ID:
293                return Channels.CONTENT_ITEM_TYPE;
294            case MATCH_CHANNEL_ID_LOGO:
295                return "image/png";
296            case MATCH_CHANNEL_ID_PROGRAM:
297                return Programs.CONTENT_TYPE;
298            case MATCH_INPUT_PACKAGE_SERVICE_CHANNEL:
299                return Channels.CONTENT_TYPE;
300            case MATCH_PROGRAM:
301                return Programs.CONTENT_TYPE;
302            case MATCH_PROGRAM_ID:
303                return Programs.CONTENT_ITEM_TYPE;
304            case MATCH_WATCHED_PROGRAM:
305                return WatchedPrograms.CONTENT_TYPE;
306            case MATCH_WATCHED_PROGRAM_ID:
307                return WatchedPrograms.CONTENT_ITEM_TYPE;
308            default:
309                throw new IllegalArgumentException("Unknown URI " + uri);
310        }
311    }
312
313    @Override
314    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
315            String sortOrder) {
316        if (needsToLimitPackage(uri)) {
317            if (!TextUtils.isEmpty(selection)) {
318                throw new IllegalArgumentException("Selection not allowed for " + uri);
319            }
320            selection = BaseTvColumns.COLUMN_PACKAGE_NAME + "=?";
321            selectionArgs = new String[] { getCallingPackage() };
322        }
323
324        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
325        String orderBy;
326
327        switch (sUriMatcher.match(uri)) {
328            case MATCH_CHANNEL:
329                queryBuilder.setTables(CHANNELS_TABLE);
330                queryBuilder.setProjectionMap(sChannelProjectionMap);
331                orderBy = DEFAULT_CHANNELS_SORT_ORDER;
332                break;
333            case MATCH_CHANNEL_ID:
334                queryBuilder.setTables(CHANNELS_TABLE);
335                queryBuilder.setProjectionMap(sChannelProjectionMap);
336                selection = DatabaseUtils.concatenateWhere(selection, Channels._ID + "=?");
337                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
338                        uri.getLastPathSegment()
339                });
340                orderBy = DEFAULT_CHANNELS_SORT_ORDER;
341                break;
342            case MATCH_CHANNEL_ID_PROGRAM:
343                queryBuilder.setTables(PROGRAMS_TABLE);
344                queryBuilder.setProjectionMap(sProgramProjectionMap);
345                selection = DatabaseUtils.concatenateWhere(selection,
346                        Programs.COLUMN_CHANNEL_ID + "=?");
347                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
348                        TvContract.getChannelId(uri)
349                });
350                String paramStartTime = uri.getQueryParameter(TvContract.PARAM_START_TIME);
351                String paramEndTime = uri.getQueryParameter(TvContract.PARAM_END_TIME);
352                if (paramStartTime != null && paramEndTime != null) {
353                    String startTime = String.valueOf(Long.parseLong(paramStartTime));
354                    String endTime = String.valueOf(Long.parseLong(paramEndTime));
355                    selection = DatabaseUtils.concatenateWhere(selection,
356                            SELECTION_OVERLAPPED_PROGRAM);
357                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
358                            TvContract.getChannelId(uri), endTime, startTime
359                    });
360                }
361                orderBy = DEFAULT_PROGRAMS_SORT_ORDER;
362                break;
363            case MATCH_INPUT_PACKAGE_SERVICE_CHANNEL:
364                queryBuilder.setTables(CHANNELS_TABLE);
365                queryBuilder.setProjectionMap(sChannelProjectionMap);
366                boolean browsableOnly = uri.getBooleanQueryParameter(
367                        TvContract.PARAM_BROWSABLE_ONLY, true);
368                selection = DatabaseUtils.concatenateWhere(selection, SELECTION_CHANNEL_BY_INPUT
369                        + (browsableOnly ? " AND " + Channels.COLUMN_BROWSABLE + "=1" : ""));
370                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
371                        TvContract.getPackageName(uri), TvContract.getServiceName(uri)
372                });
373                orderBy = DEFAULT_CHANNELS_SORT_ORDER;
374                break;
375            case MATCH_PROGRAM:
376                queryBuilder.setTables(PROGRAMS_TABLE);
377                queryBuilder.setProjectionMap(sProgramProjectionMap);
378                orderBy = DEFAULT_PROGRAMS_SORT_ORDER;
379                break;
380            case MATCH_PROGRAM_ID:
381                queryBuilder.setTables(PROGRAMS_TABLE);
382                queryBuilder.setProjectionMap(sProgramProjectionMap);
383                selection = DatabaseUtils.concatenateWhere(selection, Programs._ID + "=?");
384                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
385                        uri.getLastPathSegment()
386                });
387                orderBy = DEFAULT_PROGRAMS_SORT_ORDER;
388                break;
389            case MATCH_WATCHED_PROGRAM:
390                queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
391                queryBuilder.setProjectionMap(sWatchedProgramProjectionMap);
392                orderBy = DEFAULT_WATCHED_PROGRAMS_SORT_ORDER;
393                break;
394            case MATCH_WATCHED_PROGRAM_ID:
395                queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
396                queryBuilder.setProjectionMap(sWatchedProgramProjectionMap);
397                selection = DatabaseUtils.concatenateWhere(selection, WatchedPrograms._ID + "=?");
398                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
399                        uri.getLastPathSegment()
400                });
401                orderBy = DEFAULT_WATCHED_PROGRAMS_SORT_ORDER;
402                break;
403            default:
404                throw new IllegalArgumentException("Unknown URI " + uri);
405        }
406
407        // Use the default sort order only if no sort order is specified.
408        if (!TextUtils.isEmpty(sortOrder)) {
409            orderBy = sortOrder;
410        }
411
412        // Get the database and run the query.
413        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
414        Cursor c = queryBuilder.query(db, projection, selection, selectionArgs, null, null,
415                orderBy);
416
417        // Tell the cursor what URI to watch, so it knows when its source data changes.
418        c.setNotificationUri(getContext().getContentResolver(), uri);
419        return c;
420    }
421
422    @Override
423    public Uri insert(Uri uri, ContentValues values) {
424        switch (sUriMatcher.match(uri)) {
425            case MATCH_CHANNEL:
426            case MATCH_CHANNEL_ID:
427                return insertChannel(uri, values);
428            case MATCH_PROGRAM:
429            case MATCH_PROGRAM_ID:
430                return insertProgram(uri, values);
431            case MATCH_WATCHED_PROGRAM:
432            case MATCH_WATCHED_PROGRAM_ID:
433                return insertWatchedProgram(uri, values);
434            default:
435                throw new IllegalArgumentException("Unknown URI " + uri);
436        }
437    }
438
439    private Uri insertChannel(Uri uri, ContentValues values) {
440        validateServiceName(values.getAsString(Channels.COLUMN_SERVICE_NAME));
441
442        // Mark the owner package of this channel.
443        values.put(Channels.COLUMN_PACKAGE_NAME, getCallingPackage());
444
445        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
446        long rowId = db.insert(CHANNELS_TABLE, null, values);
447        if (rowId > 0) {
448            Uri channelUri = TvContract.buildChannelUri(rowId);
449            notifyChange(channelUri);
450            return channelUri;
451        }
452
453        throw new SQLException("Failed to insert row into " + uri);
454    }
455
456    private Uri insertProgram(Uri uri, ContentValues values) {
457        // Mark the owner package of this program.
458        values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage());
459
460        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
461        long rowId = db.insert(PROGRAMS_TABLE, null, values);
462        if (rowId > 0) {
463            Uri programUri = TvContract.buildProgramUri(rowId);
464            notifyChange(programUri);
465            return programUri;
466        }
467
468        throw new SQLException("Failed to insert row into " + uri);
469    }
470
471    private Uri insertWatchedProgram(Uri uri, ContentValues values) {
472        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
473        long rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values);
474        if (rowId > 0) {
475            Uri watchedProgramUri = TvContract.buildWatchedProgramUri(rowId);
476            notifyChange(watchedProgramUri);
477            return watchedProgramUri;
478        }
479
480        throw new SQLException("Failed to insert row into " + uri);
481    }
482
483    @Override
484    public int delete(Uri uri, String selection, String[] selectionArgs) {
485        if (needsToLimitPackage(uri)) {
486            if (!TextUtils.isEmpty(selection)) {
487                throw new IllegalArgumentException("Selection not allowed for " + uri);
488            }
489            selection = BaseTvColumns.COLUMN_PACKAGE_NAME + "=?";
490            selectionArgs = new String[] { getCallingPackage() };
491        }
492
493        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
494        int count = 0;
495
496        switch (sUriMatcher.match(uri)) {
497            case MATCH_CHANNEL:
498                count = db.delete(CHANNELS_TABLE, selection, selectionArgs);
499                break;
500            case MATCH_CHANNEL_ID:
501                selection = DatabaseUtils.concatenateWhere(selection, Channels._ID + "=?");
502                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
503                        uri.getLastPathSegment()
504                });
505                count = db.delete(CHANNELS_TABLE, selection, selectionArgs);
506                break;
507            case MATCH_CHANNEL_ID_PROGRAM:
508                String paramStartTime = uri.getQueryParameter(TvContract.PARAM_START_TIME);
509                String paramEndTime = uri.getQueryParameter(TvContract.PARAM_END_TIME);
510                if (paramStartTime != null && paramEndTime != null) {
511                    String startTime = String.valueOf(Long.parseLong(paramStartTime));
512                    String endTime = String.valueOf(Long.parseLong(paramEndTime));
513                    selection = DatabaseUtils.concatenateWhere(selection,
514                            SELECTION_OVERLAPPED_PROGRAM);
515                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
516                            TvContract.getChannelId(uri), endTime, startTime
517                    });
518                    count = db.delete(PROGRAMS_TABLE, selection, selectionArgs);
519                    if (count > 1) {
520                        Log.e(TAG, "Deleted more than one current program");
521                    }
522                } else {
523                    selection = DatabaseUtils.concatenateWhere(selection, Programs.COLUMN_CHANNEL_ID
524                            + "=?");
525                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
526                            TvContract.getChannelId(uri)
527                    });
528                    count = db.delete(PROGRAMS_TABLE, selection, selectionArgs);
529                }
530                break;
531            case MATCH_INPUT_PACKAGE_SERVICE_CHANNEL:
532                boolean browsableOnly = uri.getBooleanQueryParameter(
533                        TvContract.PARAM_BROWSABLE_ONLY, true);
534                selection = DatabaseUtils.concatenateWhere(selection, SELECTION_CHANNEL_BY_INPUT
535                        + (browsableOnly ? " AND " + Channels.COLUMN_BROWSABLE + "=1" : ""));
536                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
537                        TvContract.getPackageName(uri), TvContract.getServiceName(uri)
538                });
539                count = db.delete(CHANNELS_TABLE, selection, selectionArgs);
540                break;
541            case MATCH_PROGRAM:
542                count = db.delete(PROGRAMS_TABLE, selection, selectionArgs);
543                break;
544            case MATCH_PROGRAM_ID:
545                selection = DatabaseUtils.concatenateWhere(selection, Programs._ID + "=?");
546                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
547                        uri.getLastPathSegment()
548                });
549                count = db.delete(PROGRAMS_TABLE, selection, selectionArgs);
550                break;
551            case MATCH_WATCHED_PROGRAM:
552                count = db.delete(WATCHED_PROGRAMS_TABLE, selection, selectionArgs);
553                break;
554            case MATCH_WATCHED_PROGRAM_ID:
555                selection = DatabaseUtils.concatenateWhere(selection, WatchedPrograms._ID + "=?");
556                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
557                        uri.getLastPathSegment()
558                });
559                count = db.delete(WATCHED_PROGRAMS_TABLE, selection, selectionArgs);
560                break;
561            default:
562                throw new IllegalArgumentException("Unknown URI " + uri);
563        }
564
565        if (count > 0) {
566            notifyChange(uri);
567        }
568        return count;
569    }
570
571    @Override
572    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
573        if (needsToLimitPackage(uri)) {
574            if (!TextUtils.isEmpty(selection)) {
575                throw new IllegalArgumentException("Selection not allowed for " + uri);
576            }
577            selection = BaseTvColumns.COLUMN_PACKAGE_NAME + "=?";
578            selectionArgs = new String[] { getCallingPackage() };
579        }
580
581        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
582        int count = 0;
583
584        switch (sUriMatcher.match(uri)) {
585            case MATCH_CHANNEL:
586                count = db.update(CHANNELS_TABLE, values, selection, selectionArgs);
587                break;
588            case MATCH_CHANNEL_ID:
589                selection = DatabaseUtils.concatenateWhere(selection, Channels._ID + "=?");
590                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
591                        uri.getLastPathSegment()
592                });
593                count = db.update(CHANNELS_TABLE, values, selection, selectionArgs);
594                break;
595            case MATCH_CHANNEL_ID_PROGRAM:
596                String paramStartTime = uri.getQueryParameter(TvContract.PARAM_START_TIME);
597                String paramEndTime = uri.getQueryParameter(TvContract.PARAM_END_TIME);
598                if (paramStartTime != null && paramEndTime != null) {
599                    String startTime = String.valueOf(Long.parseLong(paramStartTime));
600                    String endTime = String.valueOf(Long.parseLong(paramEndTime));
601                    selection = DatabaseUtils.concatenateWhere(selection,
602                            SELECTION_OVERLAPPED_PROGRAM);
603                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
604                            TvContract.getChannelId(uri), endTime, startTime
605                    });
606                    count = db.update(PROGRAMS_TABLE, values, selection, selectionArgs);
607                    if (count > 1) {
608                        Log.e(TAG, "Updated more than one current program");
609                    }
610                } else {
611                    selection = DatabaseUtils.concatenateWhere(selection, Programs.COLUMN_CHANNEL_ID
612                            + "=?");
613                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
614                            TvContract.getChannelId(uri)
615                    });
616                    count = db.update(PROGRAMS_TABLE, values, selection, selectionArgs);
617                }
618                break;
619            case MATCH_INPUT_PACKAGE_SERVICE_CHANNEL:
620                boolean browsableOnly = uri.getBooleanQueryParameter(
621                        TvContract.PARAM_BROWSABLE_ONLY, true);
622                selection = DatabaseUtils.concatenateWhere(selection, SELECTION_CHANNEL_BY_INPUT
623                        + (browsableOnly ? " AND " + Channels.COLUMN_BROWSABLE + "=1" : ""));
624                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
625                        TvContract.getPackageName(uri), TvContract.getServiceName(uri)
626                });
627                count = db.update(CHANNELS_TABLE, values, selection, selectionArgs);
628                break;
629            case MATCH_PROGRAM:
630                count = db.update(PROGRAMS_TABLE, values, selection, selectionArgs);
631                break;
632            case MATCH_PROGRAM_ID:
633                selection = DatabaseUtils.concatenateWhere(selection, Programs._ID + "=?");
634                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
635                        uri.getLastPathSegment()
636                });
637                count = db.update(PROGRAMS_TABLE, values, selection, selectionArgs);
638                break;
639            case MATCH_WATCHED_PROGRAM:
640                count = db.update(WATCHED_PROGRAMS_TABLE, values, selection, selectionArgs);
641                break;
642            case MATCH_WATCHED_PROGRAM_ID:
643                selection = DatabaseUtils.concatenateWhere(selection, WatchedPrograms._ID + "=?");
644                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, new String[] {
645                        uri.getLastPathSegment()
646                });
647                count = db.update(WATCHED_PROGRAMS_TABLE, values, selection, selectionArgs);
648                break;
649            default:
650                throw new IllegalArgumentException("Unknown URI " + uri);
651        }
652
653        if (count > 0) {
654            notifyChange(uri);
655        }
656        return count;
657    }
658
659    // We might have more than one thread trying to make its way through applyBatch() so the
660    // notification coalescing needs to be thread-local to work correctly.
661    private final ThreadLocal<Set<Uri>> mTLBatchNotifications =
662            new ThreadLocal<Set<Uri>>();
663
664    private Set<Uri> getBatchNotificationsSet() {
665        return mTLBatchNotifications.get();
666    }
667
668    private void setBatchNotificationsSet(Set<Uri> batchNotifications) {
669        mTLBatchNotifications.set(batchNotifications);
670    }
671
672    @Override
673    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
674            throws OperationApplicationException {
675        setBatchNotificationsSet(Sets.<Uri>newHashSet());
676        Context context = getContext();
677        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
678        db.beginTransaction();
679        try {
680            ContentProviderResult[] results = super.applyBatch(operations);
681            db.setTransactionSuccessful();
682            return results;
683        } finally {
684            db.endTransaction();
685            final Set<Uri> notifications = getBatchNotificationsSet();
686            setBatchNotificationsSet(null);
687            for (final Uri uri : notifications) {
688                context.getContentResolver().notifyChange(uri, null);
689            }
690        }
691    }
692
693    private void notifyChange(Uri uri) {
694        final Set<Uri> batchNotifications = getBatchNotificationsSet();
695        if (batchNotifications != null) {
696            batchNotifications.add(uri);
697        } else {
698            getContext().getContentResolver().notifyChange(uri, null);
699        }
700    }
701
702    private boolean needsToLimitPackage(Uri uri) {
703        // If an application is trying to access channel or program data, we need to ensure that the
704        // access is limited to only those data entries that the application provided in the first
705        // place. The only exception is when the application has the full data access. Note that the
706        // user's watch log is treated separately with a special permission.
707        int match = sUriMatcher.match(uri);
708        return match != MATCH_WATCHED_PROGRAM && match != MATCH_WATCHED_PROGRAM_ID
709                && !callerHasFullEpgAccess();
710    }
711
712    private boolean callerHasFullEpgAccess() {
713        return getContext().checkCallingPermission(PERMISSION_ALL_EPG_DATA)
714                == PackageManager.PERMISSION_GRANTED;
715    }
716
717    private void validateServiceName(String serviceName) {
718        String packageName = getCallingPackage();
719        ComponentName componentName = new ComponentName(packageName, serviceName);
720        try {
721            getContext().getPackageManager().getServiceInfo(componentName, 0);
722        } catch (PackageManager.NameNotFoundException e) {
723            throw new IllegalArgumentException("Invalid service name: " + serviceName);
724        }
725    }
726
727    @Override
728    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
729        switch (sUriMatcher.match(uri)) {
730            case MATCH_CHANNEL_ID_LOGO:
731                return openLogoFile(uri, mode);
732            default:
733                throw new FileNotFoundException(uri.toString());
734        }
735    }
736
737    private ParcelFileDescriptor openLogoFile(Uri uri, String mode) throws FileNotFoundException {
738        long channelId = Long.parseLong(uri.getPathSegments().get(1));
739
740        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
741        queryBuilder.setTables(CHANNELS_TABLE);
742
743        String selection = Channels._ID + "=?";
744        String[] selectionArgs = new String[] { String.valueOf(channelId) };
745        if (!callerHasFullEpgAccess()) {
746            selection = DatabaseUtils.concatenateWhere(
747                    selection, Channels.COLUMN_PACKAGE_NAME + "=?");
748            selectionArgs = DatabaseUtils.appendSelectionArgs(
749                    selectionArgs, new String[] { getCallingPackage() });
750        }
751
752        // We don't write the database here.
753        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
754        if (mode.equals("r")) {
755            String sql = queryBuilder.buildQuery(
756                    new String[] { CHANNELS_COLUMN_LOGO }, selection, null, null, null, null);
757            return DatabaseUtils.blobFileDescriptorForQuery(db, sql, selectionArgs);
758        } else {
759            Cursor cursor = queryBuilder.query(
760                    db, new String[] { Channels._ID }, selection, selectionArgs, null, null, null);
761            try {
762                if (cursor.getCount() < 1) {
763                    // Fails early if corresponding channel does not exist.
764                    // PipeMonitor may still fail to update DB later.
765                    throw new FileNotFoundException(uri.toString());
766                }
767            } finally {
768                cursor.close();
769            }
770
771            try {
772                ParcelFileDescriptor[] pipeFds = ParcelFileDescriptor.createPipe();
773                PipeMonitor pipeMonitor = new PipeMonitor(
774                        pipeFds[0], channelId, selection, selectionArgs);
775                pipeMonitor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
776                return pipeFds[1];
777            } catch (IOException ioe) {
778                FileNotFoundException fne = new FileNotFoundException(uri.toString());
779                fne.initCause(ioe);
780                throw fne;
781            }
782        }
783    }
784
785    private class PipeMonitor extends AsyncTask<Void, Void, Void> {
786        private final ParcelFileDescriptor mPfd;
787        private final long mChannelId;
788        private final String mSelection;
789        private final String[] mSelectionArgs;
790
791        private PipeMonitor(ParcelFileDescriptor pfd, long channelId,
792                String selection, String[] selectionArgs) {
793            mPfd = pfd;
794            mChannelId = channelId;
795            mSelection = selection;
796            mSelectionArgs = selectionArgs;
797        }
798
799        @Override
800        protected Void doInBackground(Void... params) {
801            AutoCloseInputStream is = new AutoCloseInputStream(mPfd);
802            ByteArrayOutputStream baos = null;
803            int count = 0;
804            try {
805                Bitmap bitmap = BitmapFactory.decodeStream(is);
806                if (bitmap == null) {
807                    Log.e(TAG, "Failed to decode logo image for channel ID " + mChannelId);
808                    return null;
809                }
810
811                float scaleFactor = Math.min(1f, ((float) MAX_LOGO_IMAGE_SIZE) /
812                        Math.max(bitmap.getWidth(), bitmap.getHeight()));
813                if (scaleFactor < 1f) {
814                    bitmap = Bitmap.createScaledBitmap(bitmap,
815                            (int) (bitmap.getWidth() * scaleFactor),
816                            (int) (bitmap.getHeight() * scaleFactor), false);
817                }
818
819                baos = new ByteArrayOutputStream();
820                bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
821                byte[] bytes = baos.toByteArray();
822
823                ContentValues values = new ContentValues();
824                values.put(CHANNELS_COLUMN_LOGO, bytes);
825
826                SQLiteDatabase db = mOpenHelper.getWritableDatabase();
827                count = db.update(CHANNELS_TABLE, values, mSelection, mSelectionArgs);
828                if (count > 0) {
829                    Uri uri = TvContract.buildChannelLogoUri(mChannelId);
830                    notifyChange(uri);
831                }
832            } finally {
833                if (count == 0) {
834                    try {
835                        mPfd.closeWithError("Failed to write logo for channel ID " + mChannelId);
836                    } catch (IOException ioe) {
837                        Log.e(TAG, "Failed to close pipe", ioe);
838                    }
839                }
840                IoUtils.closeQuietly(baos);
841                IoUtils.closeQuietly(is);
842            }
843            return null;
844        }
845    }
846}
847