Home | History | Annotate | Download | only in tv
      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 
     17 package com.android.providers.tv;
     18 
     19 import android.annotation.SuppressLint;
     20 import android.app.AlarmManager;
     21 import android.app.PendingIntent;
     22 import android.content.ContentProvider;
     23 import android.content.ContentProviderOperation;
     24 import android.content.ContentProviderResult;
     25 import android.content.ContentValues;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.OperationApplicationException;
     29 import android.content.SharedPreferences;
     30 import android.content.UriMatcher;
     31 import android.content.pm.PackageManager;
     32 import android.database.Cursor;
     33 import android.database.DatabaseUtils;
     34 import android.database.SQLException;
     35 import android.database.sqlite.SQLiteDatabase;
     36 import android.database.sqlite.SQLiteOpenHelper;
     37 import android.database.sqlite.SQLiteQueryBuilder;
     38 import android.graphics.Bitmap;
     39 import android.graphics.BitmapFactory;
     40 import android.media.tv.TvContract;
     41 import android.media.tv.TvContract.BaseTvColumns;
     42 import android.media.tv.TvContract.Channels;
     43 import android.media.tv.TvContract.PreviewPrograms;
     44 import android.media.tv.TvContract.Programs;
     45 import android.media.tv.TvContract.Programs.Genres;
     46 import android.media.tv.TvContract.RecordedPrograms;
     47 import android.media.tv.TvContract.WatchedPrograms;
     48 import android.media.tv.TvContract.WatchNextPrograms;
     49 import android.net.Uri;
     50 import android.os.AsyncTask;
     51 import android.os.Bundle;
     52 import android.os.Handler;
     53 import android.os.Message;
     54 import android.os.ParcelFileDescriptor;
     55 import android.os.ParcelFileDescriptor.AutoCloseInputStream;
     56 import android.preference.PreferenceManager;
     57 import android.provider.BaseColumns;
     58 import android.text.TextUtils;
     59 import android.text.format.DateUtils;
     60 import android.util.Log;
     61 
     62 import com.android.internal.annotations.VisibleForTesting;
     63 import com.android.internal.os.SomeArgs;
     64 import com.android.providers.tv.util.SqlParams;
     65 
     66 import libcore.io.IoUtils;
     67 
     68 import java.io.ByteArrayOutputStream;
     69 import java.io.FileNotFoundException;
     70 import java.io.IOException;
     71 import java.util.ArrayList;
     72 import java.util.HashMap;
     73 import java.util.HashSet;
     74 import java.util.Iterator;
     75 import java.util.Map;
     76 import java.util.Set;
     77 import java.util.concurrent.ConcurrentHashMap;
     78 
     79 /**
     80  * TV content provider. The contract between this provider and applications is defined in
     81  * {@link android.media.tv.TvContract}.
     82  */
     83 public class TvProvider extends ContentProvider {
     84     private static final boolean DEBUG = false;
     85     private static final String TAG = "TvProvider";
     86 
     87     static final int DATABASE_VERSION = 34;
     88     static final String SHARED_PREF_BLOCKED_PACKAGES_KEY = "blocked_packages";
     89     static final String CHANNELS_TABLE = "channels";
     90     static final String PROGRAMS_TABLE = "programs";
     91     static final String RECORDED_PROGRAMS_TABLE = "recorded_programs";
     92     static final String PREVIEW_PROGRAMS_TABLE = "preview_programs";
     93     static final String WATCH_NEXT_PROGRAMS_TABLE = "watch_next_programs";
     94     static final String WATCHED_PROGRAMS_TABLE = "watched_programs";
     95     static final String PROGRAMS_TABLE_PACKAGE_NAME_INDEX = "programs_package_name_index";
     96     static final String PROGRAMS_TABLE_CHANNEL_ID_INDEX = "programs_channel_id_index";
     97     static final String PROGRAMS_TABLE_START_TIME_INDEX = "programs_start_time_index";
     98     static final String PROGRAMS_TABLE_END_TIME_INDEX = "programs_end_time_index";
     99     static final String WATCHED_PROGRAMS_TABLE_CHANNEL_ID_INDEX =
    100             "watched_programs_channel_id_index";
    101     // The internal column in the watched programs table to indicate whether the current log entry
    102     // is consolidated or not. Unconsolidated entries may have columns with missing data.
    103     static final String WATCHED_PROGRAMS_COLUMN_CONSOLIDATED = "consolidated";
    104     static final String CHANNELS_COLUMN_LOGO = "logo";
    105     private static final String DATABASE_NAME = "tv.db";
    106     private static final String DELETED_CHANNELS_TABLE = "deleted_channels";  // Deprecated
    107     private static final String DEFAULT_PROGRAMS_SORT_ORDER = Programs.COLUMN_START_TIME_UTC_MILLIS
    108             + " ASC";
    109     private static final String DEFAULT_WATCHED_PROGRAMS_SORT_ORDER =
    110             WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC";
    111     private static final String CHANNELS_TABLE_INNER_JOIN_PROGRAMS_TABLE = CHANNELS_TABLE
    112             + " INNER JOIN " + PROGRAMS_TABLE
    113             + " ON (" + CHANNELS_TABLE + "." + Channels._ID + "="
    114             + PROGRAMS_TABLE + "." + Programs.COLUMN_CHANNEL_ID + ")";
    115 
    116     // Operation names for createSqlParams().
    117     private static final String OP_QUERY = "query";
    118     private static final String OP_UPDATE = "update";
    119     private static final String OP_DELETE = "delete";
    120 
    121 
    122     private static final UriMatcher sUriMatcher;
    123     private static final int MATCH_CHANNEL = 1;
    124     private static final int MATCH_CHANNEL_ID = 2;
    125     private static final int MATCH_CHANNEL_ID_LOGO = 3;
    126     private static final int MATCH_PASSTHROUGH_ID = 4;
    127     private static final int MATCH_PROGRAM = 5;
    128     private static final int MATCH_PROGRAM_ID = 6;
    129     private static final int MATCH_WATCHED_PROGRAM = 7;
    130     private static final int MATCH_WATCHED_PROGRAM_ID = 8;
    131     private static final int MATCH_RECORDED_PROGRAM = 9;
    132     private static final int MATCH_RECORDED_PROGRAM_ID = 10;
    133     private static final int MATCH_PREVIEW_PROGRAM = 11;
    134     private static final int MATCH_PREVIEW_PROGRAM_ID = 12;
    135     private static final int MATCH_WATCH_NEXT_PROGRAM = 13;
    136     private static final int MATCH_WATCH_NEXT_PROGRAM_ID = 14;
    137 
    138     private static final int MAX_LOGO_IMAGE_SIZE = 256;
    139 
    140     private static final String EMPTY_STRING = "";
    141 
    142     private static final long MAX_PROGRAM_DATA_DELAY_IN_MILLIS = 10 * 1000; // 10 seconds
    143 
    144     private static final Map<String, String> sChannelProjectionMap;
    145     private static final Map<String, String> sProgramProjectionMap;
    146     private static final Map<String, String> sWatchedProgramProjectionMap;
    147     private static final Map<String, String> sRecordedProgramProjectionMap;
    148     private static final Map<String, String> sPreviewProgramProjectionMap;
    149     private static final Map<String, String> sWatchNextProgramProjectionMap;
    150     private static boolean sInitialized;
    151 
    152     static {
    153         sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    154         sUriMatcher.addURI(TvContract.AUTHORITY, "channel", MATCH_CHANNEL);
    155         sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#", MATCH_CHANNEL_ID);
    156         sUriMatcher.addURI(TvContract.AUTHORITY, "channel/#/logo", MATCH_CHANNEL_ID_LOGO);
    157         sUriMatcher.addURI(TvContract.AUTHORITY, "passthrough/*", MATCH_PASSTHROUGH_ID);
    158         sUriMatcher.addURI(TvContract.AUTHORITY, "program", MATCH_PROGRAM);
    159         sUriMatcher.addURI(TvContract.AUTHORITY, "program/#", MATCH_PROGRAM_ID);
    160         sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program", MATCH_WATCHED_PROGRAM);
    161         sUriMatcher.addURI(TvContract.AUTHORITY, "watched_program/#", MATCH_WATCHED_PROGRAM_ID);
    162         sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program", MATCH_RECORDED_PROGRAM);
    163         sUriMatcher.addURI(TvContract.AUTHORITY, "recorded_program/#", MATCH_RECORDED_PROGRAM_ID);
    164         sUriMatcher.addURI(TvContract.AUTHORITY, "preview_program", MATCH_PREVIEW_PROGRAM);
    165         sUriMatcher.addURI(TvContract.AUTHORITY, "preview_program/#", MATCH_PREVIEW_PROGRAM_ID);
    166         sUriMatcher.addURI(TvContract.AUTHORITY, "watch_next_program", MATCH_WATCH_NEXT_PROGRAM);
    167         sUriMatcher.addURI(TvContract.AUTHORITY, "watch_next_program/#",
    168                 MATCH_WATCH_NEXT_PROGRAM_ID);
    169 
    170         sChannelProjectionMap = new HashMap<>();
    171         sChannelProjectionMap.put(Channels._ID, CHANNELS_TABLE + "." + Channels._ID);
    172         sChannelProjectionMap.put(Channels.COLUMN_PACKAGE_NAME,
    173                 CHANNELS_TABLE + "." + Channels.COLUMN_PACKAGE_NAME);
    174         sChannelProjectionMap.put(Channels.COLUMN_INPUT_ID,
    175                 CHANNELS_TABLE + "." + Channels.COLUMN_INPUT_ID);
    176         sChannelProjectionMap.put(Channels.COLUMN_TYPE,
    177                 CHANNELS_TABLE + "." + Channels.COLUMN_TYPE);
    178         sChannelProjectionMap.put(Channels.COLUMN_SERVICE_TYPE,
    179                 CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_TYPE);
    180         sChannelProjectionMap.put(Channels.COLUMN_ORIGINAL_NETWORK_ID,
    181                 CHANNELS_TABLE + "." + Channels.COLUMN_ORIGINAL_NETWORK_ID);
    182         sChannelProjectionMap.put(Channels.COLUMN_TRANSPORT_STREAM_ID,
    183                 CHANNELS_TABLE + "." + Channels.COLUMN_TRANSPORT_STREAM_ID);
    184         sChannelProjectionMap.put(Channels.COLUMN_SERVICE_ID,
    185                 CHANNELS_TABLE + "." + Channels.COLUMN_SERVICE_ID);
    186         sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NUMBER,
    187                 CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NUMBER);
    188         sChannelProjectionMap.put(Channels.COLUMN_DISPLAY_NAME,
    189                 CHANNELS_TABLE + "." + Channels.COLUMN_DISPLAY_NAME);
    190         sChannelProjectionMap.put(Channels.COLUMN_NETWORK_AFFILIATION,
    191                 CHANNELS_TABLE + "." + Channels.COLUMN_NETWORK_AFFILIATION);
    192         sChannelProjectionMap.put(Channels.COLUMN_DESCRIPTION,
    193                 CHANNELS_TABLE + "." + Channels.COLUMN_DESCRIPTION);
    194         sChannelProjectionMap.put(Channels.COLUMN_VIDEO_FORMAT,
    195                 CHANNELS_TABLE + "." + Channels.COLUMN_VIDEO_FORMAT);
    196         sChannelProjectionMap.put(Channels.COLUMN_BROWSABLE,
    197                 CHANNELS_TABLE + "." + Channels.COLUMN_BROWSABLE);
    198         sChannelProjectionMap.put(Channels.COLUMN_SEARCHABLE,
    199                 CHANNELS_TABLE + "." + Channels.COLUMN_SEARCHABLE);
    200         sChannelProjectionMap.put(Channels.COLUMN_LOCKED,
    201                 CHANNELS_TABLE + "." + Channels.COLUMN_LOCKED);
    202         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_ICON_URI,
    203                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_ICON_URI);
    204         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_POSTER_ART_URI,
    205                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_POSTER_ART_URI);
    206         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_TEXT,
    207                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_TEXT);
    208         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_COLOR,
    209                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_COLOR);
    210         sChannelProjectionMap.put(Channels.COLUMN_APP_LINK_INTENT_URI,
    211                 CHANNELS_TABLE + "." + Channels.COLUMN_APP_LINK_INTENT_URI);
    212         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA,
    213                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_DATA);
    214         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1,
    215                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1);
    216         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
    217                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2);
    218         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3,
    219                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3);
    220         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4,
    221                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4);
    222         sChannelProjectionMap.put(Channels.COLUMN_VERSION_NUMBER,
    223                 CHANNELS_TABLE + "." + Channels.COLUMN_VERSION_NUMBER);
    224         sChannelProjectionMap.put(Channels.COLUMN_TRANSIENT,
    225                 CHANNELS_TABLE + "." + Channels.COLUMN_TRANSIENT);
    226         sChannelProjectionMap.put(Channels.COLUMN_INTERNAL_PROVIDER_ID,
    227                 CHANNELS_TABLE + "." + Channels.COLUMN_INTERNAL_PROVIDER_ID);
    228 
    229         sProgramProjectionMap = new HashMap<>();
    230         sProgramProjectionMap.put(Programs._ID, Programs._ID);
    231         sProgramProjectionMap.put(Programs.COLUMN_PACKAGE_NAME, Programs.COLUMN_PACKAGE_NAME);
    232         sProgramProjectionMap.put(Programs.COLUMN_CHANNEL_ID, Programs.COLUMN_CHANNEL_ID);
    233         sProgramProjectionMap.put(Programs.COLUMN_TITLE, Programs.COLUMN_TITLE);
    234         // COLUMN_SEASON_NUMBER is deprecated. Return COLUMN_SEASON_DISPLAY_NUMBER instead.
    235         sProgramProjectionMap.put(Programs.COLUMN_SEASON_NUMBER,
    236                 Programs.COLUMN_SEASON_DISPLAY_NUMBER + " AS " + Programs.COLUMN_SEASON_NUMBER);
    237         sProgramProjectionMap.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER,
    238                 Programs.COLUMN_SEASON_DISPLAY_NUMBER);
    239         sProgramProjectionMap.put(Programs.COLUMN_SEASON_TITLE, Programs.COLUMN_SEASON_TITLE);
    240         // COLUMN_EPISODE_NUMBER is deprecated. Return COLUMN_EPISODE_DISPLAY_NUMBER instead.
    241         sProgramProjectionMap.put(Programs.COLUMN_EPISODE_NUMBER,
    242                 Programs.COLUMN_EPISODE_DISPLAY_NUMBER + " AS " + Programs.COLUMN_EPISODE_NUMBER);
    243         sProgramProjectionMap.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
    244                 Programs.COLUMN_EPISODE_DISPLAY_NUMBER);
    245         sProgramProjectionMap.put(Programs.COLUMN_EPISODE_TITLE, Programs.COLUMN_EPISODE_TITLE);
    246         sProgramProjectionMap.put(Programs.COLUMN_START_TIME_UTC_MILLIS,
    247                 Programs.COLUMN_START_TIME_UTC_MILLIS);
    248         sProgramProjectionMap.put(Programs.COLUMN_END_TIME_UTC_MILLIS,
    249                 Programs.COLUMN_END_TIME_UTC_MILLIS);
    250         sProgramProjectionMap.put(Programs.COLUMN_BROADCAST_GENRE, Programs.COLUMN_BROADCAST_GENRE);
    251         sProgramProjectionMap.put(Programs.COLUMN_CANONICAL_GENRE, Programs.COLUMN_CANONICAL_GENRE);
    252         sProgramProjectionMap.put(Programs.COLUMN_SHORT_DESCRIPTION,
    253                 Programs.COLUMN_SHORT_DESCRIPTION);
    254         sProgramProjectionMap.put(Programs.COLUMN_LONG_DESCRIPTION,
    255                 Programs.COLUMN_LONG_DESCRIPTION);
    256         sProgramProjectionMap.put(Programs.COLUMN_VIDEO_WIDTH, Programs.COLUMN_VIDEO_WIDTH);
    257         sProgramProjectionMap.put(Programs.COLUMN_VIDEO_HEIGHT, Programs.COLUMN_VIDEO_HEIGHT);
    258         sProgramProjectionMap.put(Programs.COLUMN_AUDIO_LANGUAGE, Programs.COLUMN_AUDIO_LANGUAGE);
    259         sProgramProjectionMap.put(Programs.COLUMN_CONTENT_RATING, Programs.COLUMN_CONTENT_RATING);
    260         sProgramProjectionMap.put(Programs.COLUMN_POSTER_ART_URI, Programs.COLUMN_POSTER_ART_URI);
    261         sProgramProjectionMap.put(Programs.COLUMN_THUMBNAIL_URI, Programs.COLUMN_THUMBNAIL_URI);
    262         sProgramProjectionMap.put(Programs.COLUMN_SEARCHABLE, Programs.COLUMN_SEARCHABLE);
    263         sProgramProjectionMap.put(Programs.COLUMN_RECORDING_PROHIBITED,
    264                 Programs.COLUMN_RECORDING_PROHIBITED);
    265         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_DATA,
    266                 Programs.COLUMN_INTERNAL_PROVIDER_DATA);
    267         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG1,
    268                 Programs.COLUMN_INTERNAL_PROVIDER_FLAG1);
    269         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG2,
    270                 Programs.COLUMN_INTERNAL_PROVIDER_FLAG2);
    271         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG3,
    272                 Programs.COLUMN_INTERNAL_PROVIDER_FLAG3);
    273         sProgramProjectionMap.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG4,
    274                 Programs.COLUMN_INTERNAL_PROVIDER_FLAG4);
    275         sProgramProjectionMap.put(Programs.COLUMN_VERSION_NUMBER, Programs.COLUMN_VERSION_NUMBER);
    276         sProgramProjectionMap.put(Programs.COLUMN_REVIEW_RATING_STYLE,
    277                 Programs.COLUMN_REVIEW_RATING_STYLE);
    278         sProgramProjectionMap.put(Programs.COLUMN_REVIEW_RATING,
    279                 Programs.COLUMN_REVIEW_RATING);
    280 
    281         sWatchedProgramProjectionMap = new HashMap<>();
    282         sWatchedProgramProjectionMap.put(WatchedPrograms._ID, WatchedPrograms._ID);
    283         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
    284                 WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS);
    285         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS,
    286                 WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS);
    287         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_CHANNEL_ID,
    288                 WatchedPrograms.COLUMN_CHANNEL_ID);
    289         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_TITLE,
    290                 WatchedPrograms.COLUMN_TITLE);
    291         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS,
    292                 WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS);
    293         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS,
    294                 WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS);
    295         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_DESCRIPTION,
    296                 WatchedPrograms.COLUMN_DESCRIPTION);
    297         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS,
    298                 WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS);
    299         sWatchedProgramProjectionMap.put(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN,
    300                 WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN);
    301         sWatchedProgramProjectionMap.put(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED,
    302                 WATCHED_PROGRAMS_COLUMN_CONSOLIDATED);
    303 
    304         sRecordedProgramProjectionMap = new HashMap<>();
    305         sRecordedProgramProjectionMap.put(RecordedPrograms._ID, RecordedPrograms._ID);
    306         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_PACKAGE_NAME,
    307                 RecordedPrograms.COLUMN_PACKAGE_NAME);
    308         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INPUT_ID,
    309                 RecordedPrograms.COLUMN_INPUT_ID);
    310         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CHANNEL_ID,
    311                 RecordedPrograms.COLUMN_CHANNEL_ID);
    312         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_TITLE,
    313                 RecordedPrograms.COLUMN_TITLE);
    314         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER,
    315                 RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER);
    316         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEASON_TITLE,
    317                 RecordedPrograms.COLUMN_SEASON_TITLE);
    318         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER,
    319                 RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER);
    320         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_EPISODE_TITLE,
    321                 RecordedPrograms.COLUMN_EPISODE_TITLE);
    322         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
    323                 RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS);
    324         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS,
    325                 RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS);
    326         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_BROADCAST_GENRE,
    327                 RecordedPrograms.COLUMN_BROADCAST_GENRE);
    328         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CANONICAL_GENRE,
    329                 RecordedPrograms.COLUMN_CANONICAL_GENRE);
    330         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION,
    331                 RecordedPrograms.COLUMN_SHORT_DESCRIPTION);
    332         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION,
    333                 RecordedPrograms.COLUMN_LONG_DESCRIPTION);
    334         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_WIDTH,
    335                 RecordedPrograms.COLUMN_VIDEO_WIDTH);
    336         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT,
    337                 RecordedPrograms.COLUMN_VIDEO_HEIGHT);
    338         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE,
    339                 RecordedPrograms.COLUMN_AUDIO_LANGUAGE);
    340         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_CONTENT_RATING,
    341                 RecordedPrograms.COLUMN_CONTENT_RATING);
    342         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_POSTER_ART_URI,
    343                 RecordedPrograms.COLUMN_POSTER_ART_URI);
    344         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_THUMBNAIL_URI,
    345                 RecordedPrograms.COLUMN_THUMBNAIL_URI);
    346         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_SEARCHABLE,
    347                 RecordedPrograms.COLUMN_SEARCHABLE);
    348         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI,
    349                 RecordedPrograms.COLUMN_RECORDING_DATA_URI);
    350         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES,
    351                 RecordedPrograms.COLUMN_RECORDING_DATA_BYTES);
    352         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
    353                 RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS);
    354         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
    355                 RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS);
    356         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
    357                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA);
    358         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
    359                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1);
    360         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
    361                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2);
    362         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
    363                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3);
    364         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
    365                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4);
    366         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_VERSION_NUMBER,
    367                 RecordedPrograms.COLUMN_VERSION_NUMBER);
    368         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_REVIEW_RATING_STYLE,
    369                 RecordedPrograms.COLUMN_REVIEW_RATING_STYLE);
    370         sRecordedProgramProjectionMap.put(RecordedPrograms.COLUMN_REVIEW_RATING,
    371                 RecordedPrograms.COLUMN_REVIEW_RATING);
    372 
    373         sPreviewProgramProjectionMap = new HashMap<>();
    374         sPreviewProgramProjectionMap.put(PreviewPrograms._ID, PreviewPrograms._ID);
    375         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_PACKAGE_NAME,
    376                 PreviewPrograms.COLUMN_PACKAGE_NAME);
    377         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_CHANNEL_ID,
    378                 PreviewPrograms.COLUMN_CHANNEL_ID);
    379         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_TITLE,
    380                 PreviewPrograms.COLUMN_TITLE);
    381         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER,
    382                 PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER);
    383         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_SEASON_TITLE,
    384                 PreviewPrograms.COLUMN_SEASON_TITLE);
    385         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER,
    386                 PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER);
    387         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_EPISODE_TITLE,
    388                 PreviewPrograms.COLUMN_EPISODE_TITLE);
    389         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_CANONICAL_GENRE,
    390                 PreviewPrograms.COLUMN_CANONICAL_GENRE);
    391         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_SHORT_DESCRIPTION,
    392                 PreviewPrograms.COLUMN_SHORT_DESCRIPTION);
    393         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LONG_DESCRIPTION,
    394                 PreviewPrograms.COLUMN_LONG_DESCRIPTION);
    395         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_VIDEO_WIDTH,
    396                 PreviewPrograms.COLUMN_VIDEO_WIDTH);
    397         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_VIDEO_HEIGHT,
    398                 PreviewPrograms.COLUMN_VIDEO_HEIGHT);
    399         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_AUDIO_LANGUAGE,
    400                 PreviewPrograms.COLUMN_AUDIO_LANGUAGE);
    401         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_CONTENT_RATING,
    402                 PreviewPrograms.COLUMN_CONTENT_RATING);
    403         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_POSTER_ART_URI,
    404                 PreviewPrograms.COLUMN_POSTER_ART_URI);
    405         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_THUMBNAIL_URI,
    406                 PreviewPrograms.COLUMN_THUMBNAIL_URI);
    407         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_SEARCHABLE,
    408                 PreviewPrograms.COLUMN_SEARCHABLE);
    409         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
    410                 PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA);
    411         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
    412                 PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1);
    413         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
    414                 PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2);
    415         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
    416                 PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3);
    417         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
    418                 PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4);
    419         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_VERSION_NUMBER,
    420                 PreviewPrograms.COLUMN_VERSION_NUMBER);
    421         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID,
    422                 PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID);
    423         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI,
    424                 PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI);
    425         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS,
    426                 PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS);
    427         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_DURATION_MILLIS,
    428                 PreviewPrograms.COLUMN_DURATION_MILLIS);
    429         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTENT_URI,
    430                 PreviewPrograms.COLUMN_INTENT_URI);
    431         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_WEIGHT,
    432                 PreviewPrograms.COLUMN_WEIGHT);
    433         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_TRANSIENT,
    434                 PreviewPrograms.COLUMN_TRANSIENT);
    435         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_TYPE, PreviewPrograms.COLUMN_TYPE);
    436         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO,
    437                 PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO);
    438         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO,
    439                 PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO);
    440         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LOGO_URI,
    441                 PreviewPrograms.COLUMN_LOGO_URI);
    442         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_AVAILABILITY,
    443                 PreviewPrograms.COLUMN_AVAILABILITY);
    444         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_STARTING_PRICE,
    445                 PreviewPrograms.COLUMN_STARTING_PRICE);
    446         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_OFFER_PRICE,
    447                 PreviewPrograms.COLUMN_OFFER_PRICE);
    448         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_RELEASE_DATE,
    449                 PreviewPrograms.COLUMN_RELEASE_DATE);
    450         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_ITEM_COUNT,
    451                 PreviewPrograms.COLUMN_ITEM_COUNT);
    452         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_LIVE, PreviewPrograms.COLUMN_LIVE);
    453         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERACTION_TYPE,
    454                 PreviewPrograms.COLUMN_INTERACTION_TYPE);
    455         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_INTERACTION_COUNT,
    456                 PreviewPrograms.COLUMN_INTERACTION_COUNT);
    457         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_AUTHOR,
    458                 PreviewPrograms.COLUMN_AUTHOR);
    459         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_REVIEW_RATING_STYLE,
    460                 PreviewPrograms.COLUMN_REVIEW_RATING_STYLE);
    461         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_REVIEW_RATING,
    462                 PreviewPrograms.COLUMN_REVIEW_RATING);
    463         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_BROWSABLE,
    464                 PreviewPrograms.COLUMN_BROWSABLE);
    465         sPreviewProgramProjectionMap.put(PreviewPrograms.COLUMN_CONTENT_ID,
    466                 PreviewPrograms.COLUMN_CONTENT_ID);
    467 
    468         sWatchNextProgramProjectionMap = new HashMap<>();
    469         sWatchNextProgramProjectionMap.put(WatchNextPrograms._ID, WatchNextPrograms._ID);
    470         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_PACKAGE_NAME,
    471                 WatchNextPrograms.COLUMN_PACKAGE_NAME);
    472         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_TITLE,
    473                 WatchNextPrograms.COLUMN_TITLE);
    474         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER,
    475                 WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER);
    476         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_SEASON_TITLE,
    477                 WatchNextPrograms.COLUMN_SEASON_TITLE);
    478         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER,
    479                 WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER);
    480         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_EPISODE_TITLE,
    481                 WatchNextPrograms.COLUMN_EPISODE_TITLE);
    482         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_CANONICAL_GENRE,
    483                 WatchNextPrograms.COLUMN_CANONICAL_GENRE);
    484         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_SHORT_DESCRIPTION,
    485                 WatchNextPrograms.COLUMN_SHORT_DESCRIPTION);
    486         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LONG_DESCRIPTION,
    487                 WatchNextPrograms.COLUMN_LONG_DESCRIPTION);
    488         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_VIDEO_WIDTH,
    489                 WatchNextPrograms.COLUMN_VIDEO_WIDTH);
    490         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_VIDEO_HEIGHT,
    491                 WatchNextPrograms.COLUMN_VIDEO_HEIGHT);
    492         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_AUDIO_LANGUAGE,
    493                 WatchNextPrograms.COLUMN_AUDIO_LANGUAGE);
    494         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_CONTENT_RATING,
    495                 WatchNextPrograms.COLUMN_CONTENT_RATING);
    496         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_POSTER_ART_URI,
    497                 WatchNextPrograms.COLUMN_POSTER_ART_URI);
    498         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_THUMBNAIL_URI,
    499                 WatchNextPrograms.COLUMN_THUMBNAIL_URI);
    500         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_SEARCHABLE,
    501                 WatchNextPrograms.COLUMN_SEARCHABLE);
    502         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
    503                 WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA);
    504         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1,
    505                 WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1);
    506         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2,
    507                 WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2);
    508         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3,
    509                 WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3);
    510         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4,
    511                 WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4);
    512         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_VERSION_NUMBER,
    513                 WatchNextPrograms.COLUMN_VERSION_NUMBER);
    514         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID,
    515                 WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID);
    516         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI,
    517                 WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI);
    518         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS,
    519                 WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS);
    520         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_DURATION_MILLIS,
    521                 WatchNextPrograms.COLUMN_DURATION_MILLIS);
    522         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTENT_URI,
    523                 WatchNextPrograms.COLUMN_INTENT_URI);
    524         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_TRANSIENT,
    525                 WatchNextPrograms.COLUMN_TRANSIENT);
    526         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_TYPE,
    527                 WatchNextPrograms.COLUMN_TYPE);
    528         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE,
    529                 WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE);
    530         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO,
    531                 WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO);
    532         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO,
    533                 WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO);
    534         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LOGO_URI,
    535                 WatchNextPrograms.COLUMN_LOGO_URI);
    536         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_AVAILABILITY,
    537                 WatchNextPrograms.COLUMN_AVAILABILITY);
    538         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_STARTING_PRICE,
    539                 WatchNextPrograms.COLUMN_STARTING_PRICE);
    540         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_OFFER_PRICE,
    541                 WatchNextPrograms.COLUMN_OFFER_PRICE);
    542         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_RELEASE_DATE,
    543                 WatchNextPrograms.COLUMN_RELEASE_DATE);
    544         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_ITEM_COUNT,
    545                 WatchNextPrograms.COLUMN_ITEM_COUNT);
    546         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LIVE,
    547                 WatchNextPrograms.COLUMN_LIVE);
    548         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERACTION_TYPE,
    549                 WatchNextPrograms.COLUMN_INTERACTION_TYPE);
    550         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_INTERACTION_COUNT,
    551                 WatchNextPrograms.COLUMN_INTERACTION_COUNT);
    552         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_AUTHOR,
    553                 WatchNextPrograms.COLUMN_AUTHOR);
    554         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE,
    555                 WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE);
    556         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_REVIEW_RATING,
    557                 WatchNextPrograms.COLUMN_REVIEW_RATING);
    558         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_BROWSABLE,
    559                 WatchNextPrograms.COLUMN_BROWSABLE);
    560         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_CONTENT_ID,
    561                 WatchNextPrograms.COLUMN_CONTENT_ID);
    562         sWatchNextProgramProjectionMap.put(WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS,
    563                 WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS);
    564     }
    565 
    566     // Mapping from broadcast genre to canonical genre.
    567     private static Map<String, String> sGenreMap;
    568 
    569     private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
    570 
    571     private static final String PERMISSION_ACCESS_ALL_EPG_DATA =
    572             "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA";
    573 
    574     private static final String PERMISSION_ACCESS_WATCHED_PROGRAMS =
    575             "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS";
    576 
    577     private static final String CREATE_RECORDED_PROGRAMS_TABLE_SQL =
    578             "CREATE TABLE " + RECORDED_PROGRAMS_TABLE + " ("
    579             + RecordedPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
    580             + RecordedPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
    581             + RecordedPrograms.COLUMN_INPUT_ID + " TEXT NOT NULL,"
    582             + RecordedPrograms.COLUMN_CHANNEL_ID + " INTEGER,"
    583             + RecordedPrograms.COLUMN_TITLE + " TEXT,"
    584             + RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER + " TEXT,"
    585             + RecordedPrograms.COLUMN_SEASON_TITLE + " TEXT,"
    586             + RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER + " TEXT,"
    587             + RecordedPrograms.COLUMN_EPISODE_TITLE + " TEXT,"
    588             + RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS + " INTEGER,"
    589             + RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS + " INTEGER,"
    590             + RecordedPrograms.COLUMN_BROADCAST_GENRE + " TEXT,"
    591             + RecordedPrograms.COLUMN_CANONICAL_GENRE + " TEXT,"
    592             + RecordedPrograms.COLUMN_SHORT_DESCRIPTION + " TEXT,"
    593             + RecordedPrograms.COLUMN_LONG_DESCRIPTION + " TEXT,"
    594             + RecordedPrograms.COLUMN_VIDEO_WIDTH + " INTEGER,"
    595             + RecordedPrograms.COLUMN_VIDEO_HEIGHT + " INTEGER,"
    596             + RecordedPrograms.COLUMN_AUDIO_LANGUAGE + " TEXT,"
    597             + RecordedPrograms.COLUMN_CONTENT_RATING + " TEXT,"
    598             + RecordedPrograms.COLUMN_POSTER_ART_URI + " TEXT,"
    599             + RecordedPrograms.COLUMN_THUMBNAIL_URI + " TEXT,"
    600             + RecordedPrograms.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
    601             + RecordedPrograms.COLUMN_RECORDING_DATA_URI + " TEXT,"
    602             + RecordedPrograms.COLUMN_RECORDING_DATA_BYTES + " INTEGER,"
    603             + RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS + " INTEGER,"
    604             + RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS + " INTEGER,"
    605             + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
    606             + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER,"
    607             + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER,"
    608             + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER,"
    609             + RecordedPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER,"
    610             + RecordedPrograms.COLUMN_VERSION_NUMBER + " INTEGER,"
    611             + RecordedPrograms.COLUMN_REVIEW_RATING_STYLE + " INTEGER,"
    612             + RecordedPrograms.COLUMN_REVIEW_RATING + " TEXT,"
    613             + "FOREIGN KEY(" + RecordedPrograms.COLUMN_CHANNEL_ID + ") "
    614                     + "REFERENCES " + CHANNELS_TABLE + "(" + Channels._ID + ") "
    615                     + "ON UPDATE CASCADE ON DELETE SET NULL);";
    616 
    617     private static final String CREATE_PREVIEW_PROGRAMS_TABLE_SQL =
    618             "CREATE TABLE " + PREVIEW_PROGRAMS_TABLE + " ("
    619             + PreviewPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
    620             + PreviewPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
    621             + PreviewPrograms.COLUMN_CHANNEL_ID + " INTEGER,"
    622             + PreviewPrograms.COLUMN_TITLE + " TEXT,"
    623             + PreviewPrograms.COLUMN_SEASON_DISPLAY_NUMBER + " TEXT,"
    624             + PreviewPrograms.COLUMN_SEASON_TITLE + " TEXT,"
    625             + PreviewPrograms.COLUMN_EPISODE_DISPLAY_NUMBER + " TEXT,"
    626             + PreviewPrograms.COLUMN_EPISODE_TITLE + " TEXT,"
    627             + PreviewPrograms.COLUMN_CANONICAL_GENRE + " TEXT,"
    628             + PreviewPrograms.COLUMN_SHORT_DESCRIPTION + " TEXT,"
    629             + PreviewPrograms.COLUMN_LONG_DESCRIPTION + " TEXT,"
    630             + PreviewPrograms.COLUMN_VIDEO_WIDTH + " INTEGER,"
    631             + PreviewPrograms.COLUMN_VIDEO_HEIGHT + " INTEGER,"
    632             + PreviewPrograms.COLUMN_AUDIO_LANGUAGE + " TEXT,"
    633             + PreviewPrograms.COLUMN_CONTENT_RATING + " TEXT,"
    634             + PreviewPrograms.COLUMN_POSTER_ART_URI + " TEXT,"
    635             + PreviewPrograms.COLUMN_THUMBNAIL_URI + " TEXT,"
    636             + PreviewPrograms.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
    637             + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
    638             + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER,"
    639             + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER,"
    640             + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER,"
    641             + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER,"
    642             + PreviewPrograms.COLUMN_VERSION_NUMBER + " INTEGER,"
    643             + PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID + " TEXT,"
    644             + PreviewPrograms.COLUMN_PREVIEW_VIDEO_URI + " TEXT,"
    645             + PreviewPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS + " INTEGER,"
    646             + PreviewPrograms.COLUMN_DURATION_MILLIS + " INTEGER,"
    647             + PreviewPrograms.COLUMN_INTENT_URI + " TEXT,"
    648             + PreviewPrograms.COLUMN_WEIGHT + " INTEGER,"
    649             + PreviewPrograms.COLUMN_TRANSIENT + " INTEGER NOT NULL DEFAULT 0,"
    650             + PreviewPrograms.COLUMN_TYPE + " INTEGER NOT NULL,"
    651             + PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO + " INTEGER,"
    652             + PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO + " INTEGER,"
    653             + PreviewPrograms.COLUMN_LOGO_URI + " TEXT,"
    654             + PreviewPrograms.COLUMN_AVAILABILITY + " INTERGER,"
    655             + PreviewPrograms.COLUMN_STARTING_PRICE + " TEXT,"
    656             + PreviewPrograms.COLUMN_OFFER_PRICE + " TEXT,"
    657             + PreviewPrograms.COLUMN_RELEASE_DATE + " TEXT,"
    658             + PreviewPrograms.COLUMN_ITEM_COUNT + " INTEGER,"
    659             + PreviewPrograms.COLUMN_LIVE + " INTEGER NOT NULL DEFAULT 0,"
    660             + PreviewPrograms.COLUMN_INTERACTION_TYPE + " INTEGER,"
    661             + PreviewPrograms.COLUMN_INTERACTION_COUNT + " INTEGER,"
    662             + PreviewPrograms.COLUMN_AUTHOR + " TEXT,"
    663             + PreviewPrograms.COLUMN_REVIEW_RATING_STYLE + " INTEGER,"
    664             + PreviewPrograms.COLUMN_REVIEW_RATING + " TEXT,"
    665             + PreviewPrograms.COLUMN_BROWSABLE + " INTEGER NOT NULL DEFAULT 1,"
    666             + PreviewPrograms.COLUMN_CONTENT_ID + " TEXT,"
    667             + "FOREIGN KEY("
    668                     + PreviewPrograms.COLUMN_CHANNEL_ID + "," + PreviewPrograms.COLUMN_PACKAGE_NAME
    669                     + ") REFERENCES " + CHANNELS_TABLE + "("
    670                     + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME
    671                     + ") ON UPDATE CASCADE ON DELETE CASCADE"
    672                     + ");";
    673     private static final String CREATE_PREVIEW_PROGRAMS_PACKAGE_NAME_INDEX_SQL =
    674             "CREATE INDEX preview_programs_package_name_index ON " + PREVIEW_PROGRAMS_TABLE
    675             + "(" + PreviewPrograms.COLUMN_PACKAGE_NAME + ");";
    676     private static final String CREATE_PREVIEW_PROGRAMS_CHANNEL_ID_INDEX_SQL =
    677             "CREATE INDEX preview_programs_id_index ON " + PREVIEW_PROGRAMS_TABLE
    678             + "(" + PreviewPrograms.COLUMN_CHANNEL_ID + ");";
    679     private static final String CREATE_WATCH_NEXT_PROGRAMS_TABLE_SQL =
    680             "CREATE TABLE " + WATCH_NEXT_PROGRAMS_TABLE + " ("
    681             + WatchNextPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
    682             + WatchNextPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
    683             + WatchNextPrograms.COLUMN_TITLE + " TEXT,"
    684             + WatchNextPrograms.COLUMN_SEASON_DISPLAY_NUMBER + " TEXT,"
    685             + WatchNextPrograms.COLUMN_SEASON_TITLE + " TEXT,"
    686             + WatchNextPrograms.COLUMN_EPISODE_DISPLAY_NUMBER + " TEXT,"
    687             + WatchNextPrograms.COLUMN_EPISODE_TITLE + " TEXT,"
    688             + WatchNextPrograms.COLUMN_CANONICAL_GENRE + " TEXT,"
    689             + WatchNextPrograms.COLUMN_SHORT_DESCRIPTION + " TEXT,"
    690             + WatchNextPrograms.COLUMN_LONG_DESCRIPTION + " TEXT,"
    691             + WatchNextPrograms.COLUMN_VIDEO_WIDTH + " INTEGER,"
    692             + WatchNextPrograms.COLUMN_VIDEO_HEIGHT + " INTEGER,"
    693             + WatchNextPrograms.COLUMN_AUDIO_LANGUAGE + " TEXT,"
    694             + WatchNextPrograms.COLUMN_CONTENT_RATING + " TEXT,"
    695             + WatchNextPrograms.COLUMN_POSTER_ART_URI + " TEXT,"
    696             + WatchNextPrograms.COLUMN_THUMBNAIL_URI + " TEXT,"
    697             + WatchNextPrograms.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
    698             + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
    699             + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER,"
    700             + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER,"
    701             + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER,"
    702             + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER,"
    703             + WatchNextPrograms.COLUMN_VERSION_NUMBER + " INTEGER,"
    704             + WatchNextPrograms.COLUMN_INTERNAL_PROVIDER_ID + " TEXT,"
    705             + WatchNextPrograms.COLUMN_PREVIEW_VIDEO_URI + " TEXT,"
    706             + WatchNextPrograms.COLUMN_LAST_PLAYBACK_POSITION_MILLIS + " INTEGER,"
    707             + WatchNextPrograms.COLUMN_DURATION_MILLIS + " INTEGER,"
    708             + WatchNextPrograms.COLUMN_INTENT_URI + " TEXT,"
    709             + WatchNextPrograms.COLUMN_TRANSIENT + " INTEGER NOT NULL DEFAULT 0,"
    710             + WatchNextPrograms.COLUMN_TYPE + " INTEGER NOT NULL,"
    711             + WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE + " INTEGER,"
    712             + WatchNextPrograms.COLUMN_POSTER_ART_ASPECT_RATIO + " INTEGER,"
    713             + WatchNextPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO + " INTEGER,"
    714             + WatchNextPrograms.COLUMN_LOGO_URI + " TEXT,"
    715             + WatchNextPrograms.COLUMN_AVAILABILITY + " INTEGER,"
    716             + WatchNextPrograms.COLUMN_STARTING_PRICE + " TEXT,"
    717             + WatchNextPrograms.COLUMN_OFFER_PRICE + " TEXT,"
    718             + WatchNextPrograms.COLUMN_RELEASE_DATE + " TEXT,"
    719             + WatchNextPrograms.COLUMN_ITEM_COUNT + " INTEGER,"
    720             + WatchNextPrograms.COLUMN_LIVE + " INTEGER NOT NULL DEFAULT 0,"
    721             + WatchNextPrograms.COLUMN_INTERACTION_TYPE + " INTEGER,"
    722             + WatchNextPrograms.COLUMN_INTERACTION_COUNT + " INTEGER,"
    723             + WatchNextPrograms.COLUMN_AUTHOR + " TEXT,"
    724             + WatchNextPrograms.COLUMN_REVIEW_RATING_STYLE + " INTEGER,"
    725             + WatchNextPrograms.COLUMN_REVIEW_RATING + " TEXT,"
    726             + WatchNextPrograms.COLUMN_BROWSABLE + " INTEGER NOT NULL DEFAULT 1,"
    727             + WatchNextPrograms.COLUMN_CONTENT_ID + " TEXT,"
    728             + WatchNextPrograms.COLUMN_LAST_ENGAGEMENT_TIME_UTC_MILLIS + " INTEGER"
    729             + ");";
    730     private static final String CREATE_WATCH_NEXT_PROGRAMS_PACKAGE_NAME_INDEX_SQL =
    731             "CREATE INDEX watch_next_programs_package_name_index ON " + WATCH_NEXT_PROGRAMS_TABLE
    732             + "(" + WatchNextPrograms.COLUMN_PACKAGE_NAME + ");";
    733 
    734     static class DatabaseHelper extends SQLiteOpenHelper {
    735         private static DatabaseHelper sSingleton = null;
    736         private static Context mContext;
    737 
    738         public static synchronized DatabaseHelper getInstance(Context context) {
    739             if (sSingleton == null) {
    740                 sSingleton = new DatabaseHelper(context);
    741             }
    742             return sSingleton;
    743         }
    744 
    745         private DatabaseHelper(Context context) {
    746             this(context, DATABASE_NAME, DATABASE_VERSION);
    747         }
    748 
    749         @VisibleForTesting
    750         DatabaseHelper(Context context, String databaseName, int databaseVersion) {
    751             super(context, databaseName, null, databaseVersion);
    752             mContext = context;
    753         }
    754 
    755         @Override
    756         public void onConfigure(SQLiteDatabase db) {
    757             db.setForeignKeyConstraintsEnabled(true);
    758         }
    759 
    760         @Override
    761         public void onCreate(SQLiteDatabase db) {
    762             if (DEBUG) {
    763                 Log.d(TAG, "Creating database");
    764             }
    765             // Set up the database schema.
    766             db.execSQL("CREATE TABLE " + CHANNELS_TABLE + " ("
    767                     + Channels._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
    768                     + Channels.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
    769                     + Channels.COLUMN_INPUT_ID + " TEXT NOT NULL,"
    770                     + Channels.COLUMN_TYPE + " TEXT NOT NULL DEFAULT '" + Channels.TYPE_OTHER + "',"
    771                     + Channels.COLUMN_SERVICE_TYPE + " TEXT NOT NULL DEFAULT '"
    772                     + Channels.SERVICE_TYPE_AUDIO_VIDEO + "',"
    773                     + Channels.COLUMN_ORIGINAL_NETWORK_ID + " INTEGER NOT NULL DEFAULT 0,"
    774                     + Channels.COLUMN_TRANSPORT_STREAM_ID + " INTEGER NOT NULL DEFAULT 0,"
    775                     + Channels.COLUMN_SERVICE_ID + " INTEGER NOT NULL DEFAULT 0,"
    776                     + Channels.COLUMN_DISPLAY_NUMBER + " TEXT,"
    777                     + Channels.COLUMN_DISPLAY_NAME + " TEXT,"
    778                     + Channels.COLUMN_NETWORK_AFFILIATION + " TEXT,"
    779                     + Channels.COLUMN_DESCRIPTION + " TEXT,"
    780                     + Channels.COLUMN_VIDEO_FORMAT + " TEXT,"
    781                     + Channels.COLUMN_BROWSABLE + " INTEGER NOT NULL DEFAULT 0,"
    782                     + Channels.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
    783                     + Channels.COLUMN_LOCKED + " INTEGER NOT NULL DEFAULT 0,"
    784                     + Channels.COLUMN_APP_LINK_ICON_URI + " TEXT,"
    785                     + Channels.COLUMN_APP_LINK_POSTER_ART_URI + " TEXT,"
    786                     + Channels.COLUMN_APP_LINK_TEXT + " TEXT,"
    787                     + Channels.COLUMN_APP_LINK_COLOR + " INTEGER,"
    788                     + Channels.COLUMN_APP_LINK_INTENT_URI + " TEXT,"
    789                     + Channels.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
    790                     + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER,"
    791                     + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER,"
    792                     + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER,"
    793                     + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER,"
    794                     + CHANNELS_COLUMN_LOGO + " BLOB,"
    795                     + Channels.COLUMN_VERSION_NUMBER + " INTEGER,"
    796                     + Channels.COLUMN_TRANSIENT + " INTEGER NOT NULL DEFAULT 0,"
    797                     + Channels.COLUMN_INTERNAL_PROVIDER_ID + " TEXT,"
    798                     // Needed for foreign keys in other tables.
    799                     + "UNIQUE(" + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME + ")"
    800                     + ");");
    801             db.execSQL("CREATE TABLE " + PROGRAMS_TABLE + " ("
    802                     + Programs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
    803                     + Programs.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
    804                     + Programs.COLUMN_CHANNEL_ID + " INTEGER,"
    805                     + Programs.COLUMN_TITLE + " TEXT,"
    806                     + Programs.COLUMN_SEASON_DISPLAY_NUMBER + " TEXT,"
    807                     + Programs.COLUMN_SEASON_TITLE + " TEXT,"
    808                     + Programs.COLUMN_EPISODE_DISPLAY_NUMBER + " TEXT,"
    809                     + Programs.COLUMN_EPISODE_TITLE + " TEXT,"
    810                     + Programs.COLUMN_START_TIME_UTC_MILLIS + " INTEGER,"
    811                     + Programs.COLUMN_END_TIME_UTC_MILLIS + " INTEGER,"
    812                     + Programs.COLUMN_BROADCAST_GENRE + " TEXT,"
    813                     + Programs.COLUMN_CANONICAL_GENRE + " TEXT,"
    814                     + Programs.COLUMN_SHORT_DESCRIPTION + " TEXT,"
    815                     + Programs.COLUMN_LONG_DESCRIPTION + " TEXT,"
    816                     + Programs.COLUMN_VIDEO_WIDTH + " INTEGER,"
    817                     + Programs.COLUMN_VIDEO_HEIGHT + " INTEGER,"
    818                     + Programs.COLUMN_AUDIO_LANGUAGE + " TEXT,"
    819                     + Programs.COLUMN_CONTENT_RATING + " TEXT,"
    820                     + Programs.COLUMN_POSTER_ART_URI + " TEXT,"
    821                     + Programs.COLUMN_THUMBNAIL_URI + " TEXT,"
    822                     + Programs.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1,"
    823                     + Programs.COLUMN_RECORDING_PROHIBITED + " INTEGER NOT NULL DEFAULT 0,"
    824                     + Programs.COLUMN_INTERNAL_PROVIDER_DATA + " BLOB,"
    825                     + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER,"
    826                     + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER,"
    827                     + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER,"
    828                     + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER,"
    829                     + Programs.COLUMN_REVIEW_RATING_STYLE + " INTEGER,"
    830                     + Programs.COLUMN_REVIEW_RATING + " TEXT,"
    831                     + Programs.COLUMN_VERSION_NUMBER + " INTEGER,"
    832                     + "FOREIGN KEY("
    833                             + Programs.COLUMN_CHANNEL_ID + "," + Programs.COLUMN_PACKAGE_NAME
    834                             + ") REFERENCES " + CHANNELS_TABLE + "("
    835                             + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME
    836                             + ") ON UPDATE CASCADE ON DELETE CASCADE"
    837                     + ");");
    838             db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_PACKAGE_NAME_INDEX + " ON " + PROGRAMS_TABLE
    839                     + "(" + Programs.COLUMN_PACKAGE_NAME + ");");
    840             db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_CHANNEL_ID_INDEX + " ON " + PROGRAMS_TABLE
    841                     + "(" + Programs.COLUMN_CHANNEL_ID + ");");
    842             db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_START_TIME_INDEX + " ON " + PROGRAMS_TABLE
    843                     + "(" + Programs.COLUMN_START_TIME_UTC_MILLIS + ");");
    844             db.execSQL("CREATE INDEX " + PROGRAMS_TABLE_END_TIME_INDEX + " ON " + PROGRAMS_TABLE
    845                     + "(" + Programs.COLUMN_END_TIME_UTC_MILLIS + ");");
    846             db.execSQL("CREATE TABLE " + WATCHED_PROGRAMS_TABLE + " ("
    847                     + WatchedPrograms._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
    848                     + WatchedPrograms.COLUMN_PACKAGE_NAME + " TEXT NOT NULL,"
    849                     + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS
    850                     + " INTEGER NOT NULL DEFAULT 0,"
    851                     + WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS
    852                     + " INTEGER NOT NULL DEFAULT 0,"
    853                     + WatchedPrograms.COLUMN_CHANNEL_ID + " INTEGER,"
    854                     + WatchedPrograms.COLUMN_TITLE + " TEXT,"
    855                     + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS + " INTEGER,"
    856                     + WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS + " INTEGER,"
    857                     + WatchedPrograms.COLUMN_DESCRIPTION + " TEXT,"
    858                     + WatchedPrograms.COLUMN_INTERNAL_TUNE_PARAMS + " TEXT,"
    859                     + WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + " TEXT NOT NULL,"
    860                     + WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + " INTEGER NOT NULL DEFAULT 0,"
    861                     + "FOREIGN KEY("
    862                             + WatchedPrograms.COLUMN_CHANNEL_ID + ","
    863                             + WatchedPrograms.COLUMN_PACKAGE_NAME
    864                             + ") REFERENCES " + CHANNELS_TABLE + "("
    865                             + Channels._ID + "," + Channels.COLUMN_PACKAGE_NAME
    866                             + ") ON UPDATE CASCADE ON DELETE CASCADE"
    867                     + ");");
    868             db.execSQL("CREATE INDEX " + WATCHED_PROGRAMS_TABLE_CHANNEL_ID_INDEX + " ON "
    869                     + WATCHED_PROGRAMS_TABLE + "(" + WatchedPrograms.COLUMN_CHANNEL_ID + ");");
    870             db.execSQL(CREATE_RECORDED_PROGRAMS_TABLE_SQL);
    871             db.execSQL(CREATE_PREVIEW_PROGRAMS_TABLE_SQL);
    872             db.execSQL(CREATE_PREVIEW_PROGRAMS_PACKAGE_NAME_INDEX_SQL);
    873             db.execSQL(CREATE_PREVIEW_PROGRAMS_CHANNEL_ID_INDEX_SQL);
    874             db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_TABLE_SQL);
    875             db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_PACKAGE_NAME_INDEX_SQL);
    876         }
    877 
    878         @Override
    879         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    880             if (oldVersion < 23) {
    881                 Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion
    882                         + ", data will be lost!");
    883                 db.execSQL("DROP TABLE IF EXISTS " + DELETED_CHANNELS_TABLE);
    884                 db.execSQL("DROP TABLE IF EXISTS " + WATCHED_PROGRAMS_TABLE);
    885                 db.execSQL("DROP TABLE IF EXISTS " + PROGRAMS_TABLE);
    886                 db.execSQL("DROP TABLE IF EXISTS " + CHANNELS_TABLE);
    887 
    888                 onCreate(db);
    889                 return;
    890             }
    891 
    892             Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion + ".");
    893             if (oldVersion <= 23) {
    894                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
    895                         + Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER;");
    896                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
    897                         + Channels.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER;");
    898                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
    899                         + Channels.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER;");
    900                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
    901                         + Channels.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER;");
    902             }
    903             if (oldVersion <= 24) {
    904                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
    905                         + Programs.COLUMN_INTERNAL_PROVIDER_FLAG1 + " INTEGER;");
    906                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
    907                         + Programs.COLUMN_INTERNAL_PROVIDER_FLAG2 + " INTEGER;");
    908                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
    909                         + Programs.COLUMN_INTERNAL_PROVIDER_FLAG3 + " INTEGER;");
    910                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
    911                         + Programs.COLUMN_INTERNAL_PROVIDER_FLAG4 + " INTEGER;");
    912             }
    913             if (oldVersion <= 25) {
    914                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
    915                         + Channels.COLUMN_APP_LINK_ICON_URI + " TEXT;");
    916                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
    917                         + Channels.COLUMN_APP_LINK_POSTER_ART_URI + " TEXT;");
    918                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
    919                         + Channels.COLUMN_APP_LINK_TEXT + " TEXT;");
    920                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
    921                         + Channels.COLUMN_APP_LINK_COLOR + " INTEGER;");
    922                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
    923                         + Channels.COLUMN_APP_LINK_INTENT_URI + " TEXT;");
    924                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
    925                         + Programs.COLUMN_SEARCHABLE + " INTEGER NOT NULL DEFAULT 1;");
    926             }
    927             if (oldVersion <= 28) {
    928                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
    929                         + Programs.COLUMN_SEASON_TITLE + " TEXT;");
    930                 migrateIntegerColumnToTextColumn(db, PROGRAMS_TABLE, Programs.COLUMN_SEASON_NUMBER,
    931                         Programs.COLUMN_SEASON_DISPLAY_NUMBER);
    932                 migrateIntegerColumnToTextColumn(db, PROGRAMS_TABLE, Programs.COLUMN_EPISODE_NUMBER,
    933                         Programs.COLUMN_EPISODE_DISPLAY_NUMBER);
    934             }
    935             if (oldVersion <= 29) {
    936                 db.execSQL("DROP TABLE IF EXISTS " + RECORDED_PROGRAMS_TABLE);
    937                 db.execSQL(CREATE_RECORDED_PROGRAMS_TABLE_SQL);
    938             }
    939             if (oldVersion <= 30) {
    940                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
    941                         + Programs.COLUMN_RECORDING_PROHIBITED + " INTEGER NOT NULL DEFAULT 0;");
    942             }
    943             if (oldVersion <= 32) {
    944                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
    945                         + Channels.COLUMN_TRANSIENT + " INTEGER NOT NULL DEFAULT 0;");
    946                 db.execSQL("ALTER TABLE " + CHANNELS_TABLE + " ADD "
    947                         + Channels.COLUMN_INTERNAL_PROVIDER_ID + " TEXT;");
    948                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
    949                         + Programs.COLUMN_REVIEW_RATING_STYLE + " INTEGER;");
    950                 db.execSQL("ALTER TABLE " + PROGRAMS_TABLE + " ADD "
    951                         + Programs.COLUMN_REVIEW_RATING + " TEXT;");
    952                 if (oldVersion > 29) {
    953                     db.execSQL("ALTER TABLE " + RECORDED_PROGRAMS_TABLE + " ADD "
    954                             + RecordedPrograms.COLUMN_REVIEW_RATING_STYLE + " INTEGER;");
    955                     db.execSQL("ALTER TABLE " + RECORDED_PROGRAMS_TABLE + " ADD "
    956                             + RecordedPrograms.COLUMN_REVIEW_RATING + " TEXT;");
    957                 }
    958             }
    959             if (oldVersion <= 33) {
    960                 db.execSQL("DROP TABLE IF EXISTS " + PREVIEW_PROGRAMS_TABLE);
    961                 db.execSQL("DROP TABLE IF EXISTS " + WATCH_NEXT_PROGRAMS_TABLE);
    962                 db.execSQL(CREATE_PREVIEW_PROGRAMS_TABLE_SQL);
    963                 db.execSQL(CREATE_PREVIEW_PROGRAMS_PACKAGE_NAME_INDEX_SQL);
    964                 db.execSQL(CREATE_PREVIEW_PROGRAMS_CHANNEL_ID_INDEX_SQL);
    965                 db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_TABLE_SQL);
    966                 db.execSQL(CREATE_WATCH_NEXT_PROGRAMS_PACKAGE_NAME_INDEX_SQL);
    967             }
    968             Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion + " is done.");
    969         }
    970 
    971         @Override
    972         public void onOpen(SQLiteDatabase db) {
    973             // This method is thread-safe. It's guaranteed by the implementation of SQLiteOpenHelper
    974             if (!sInitialized) {
    975                 buildProjectionMap(db);
    976                 sBlockedPackagesSharedPreference = PreferenceManager.getDefaultSharedPreferences(
    977                         mContext);
    978                 sBlockedPackages = new ConcurrentHashMap<>();
    979                 for (String packageName : sBlockedPackagesSharedPreference.getStringSet(
    980                         SHARED_PREF_BLOCKED_PACKAGES_KEY, new HashSet<>())) {
    981                     sBlockedPackages.put(packageName, true);
    982                 }
    983                 sInitialized = true;
    984             }
    985         }
    986 
    987         private void buildProjectionMap(SQLiteDatabase db) {
    988             updateProjectionMap(db, CHANNELS_TABLE, sChannelProjectionMap);
    989             updateProjectionMap(db, PROGRAMS_TABLE, sProgramProjectionMap);
    990             updateProjectionMap(db, WATCHED_PROGRAMS_TABLE, sWatchedProgramProjectionMap);
    991             updateProjectionMap(db, RECORDED_PROGRAMS_TABLE, sRecordedProgramProjectionMap);
    992             updateProjectionMap(db, PREVIEW_PROGRAMS_TABLE, sPreviewProgramProjectionMap);
    993             updateProjectionMap(db, WATCH_NEXT_PROGRAMS_TABLE, sWatchNextProgramProjectionMap);
    994         }
    995 
    996         private void updateProjectionMap(SQLiteDatabase db, String tableName,
    997                 Map<String, String> projectionMap) {
    998             try(Cursor cursor = db.rawQuery("SELECT * FROM " + tableName + " LIMIT 0", null)) {
    999                 for (String columnName : cursor.getColumnNames()) {
   1000                     if (!projectionMap.containsKey(columnName)) {
   1001                         projectionMap.put(columnName, tableName + '.' + columnName);
   1002                     }
   1003                 }
   1004             }
   1005         }
   1006 
   1007         private static void migrateIntegerColumnToTextColumn(SQLiteDatabase db, String table,
   1008                 String integerColumn, String textColumn) {
   1009             db.execSQL("ALTER TABLE " + table + " ADD " + textColumn + " TEXT;");
   1010             db.execSQL("UPDATE " + table + " SET " + textColumn + " = CAST(" + integerColumn
   1011                     + " AS TEXT);");
   1012         }
   1013     }
   1014 
   1015     private DatabaseHelper mOpenHelper;
   1016     private static SharedPreferences sBlockedPackagesSharedPreference;
   1017     private static Map<String, Boolean> sBlockedPackages;
   1018     @VisibleForTesting
   1019     protected TransientRowHelper mTransientRowHelper;
   1020 
   1021     private final Handler mLogHandler = new WatchLogHandler();
   1022 
   1023     @Override
   1024     public boolean onCreate() {
   1025         if (DEBUG) {
   1026             Log.d(TAG, "Creating TvProvider");
   1027         }
   1028         if (mOpenHelper == null) {
   1029             mOpenHelper = DatabaseHelper.getInstance(getContext());
   1030         }
   1031         mTransientRowHelper = TransientRowHelper.getInstance(getContext());
   1032         scheduleEpgDataCleanup();
   1033         buildGenreMap();
   1034 
   1035         // DB operation, which may trigger upgrade, should not happen in onCreate.
   1036         new AsyncTask<Void, Void, Void>() {
   1037             @Override
   1038             protected Void doInBackground(Void... params) {
   1039                 deleteUnconsolidatedWatchedProgramsRows();
   1040                 return null;
   1041             }
   1042         }.execute();
   1043         return true;
   1044     }
   1045 
   1046     @VisibleForTesting
   1047     void scheduleEpgDataCleanup() {
   1048         Intent intent = new Intent(EpgDataCleanupService.ACTION_CLEAN_UP_EPG_DATA);
   1049         intent.setClass(getContext(), EpgDataCleanupService.class);
   1050         PendingIntent pendingIntent = PendingIntent.getService(
   1051                 getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
   1052         AlarmManager alarmManager =
   1053                 (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
   1054         alarmManager.setInexactRepeating(AlarmManager.RTC, System.currentTimeMillis(),
   1055                 AlarmManager.INTERVAL_HALF_DAY, pendingIntent);
   1056     }
   1057 
   1058     private void buildGenreMap() {
   1059         if (sGenreMap != null) {
   1060             return;
   1061         }
   1062 
   1063         sGenreMap = new HashMap<>();
   1064         buildGenreMap(R.array.genre_mapping_atsc);
   1065         buildGenreMap(R.array.genre_mapping_dvb);
   1066         buildGenreMap(R.array.genre_mapping_isdb);
   1067         buildGenreMap(R.array.genre_mapping_isdb_br);
   1068     }
   1069 
   1070     @SuppressLint("DefaultLocale")
   1071     private void buildGenreMap(int id) {
   1072         String[] maps = getContext().getResources().getStringArray(id);
   1073         for (String map : maps) {
   1074             String[] arr = map.split("\\|");
   1075             if (arr.length != 2) {
   1076                 throw new IllegalArgumentException("Invalid genre mapping : " + map);
   1077             }
   1078             sGenreMap.put(arr[0].toUpperCase(), arr[1]);
   1079         }
   1080     }
   1081 
   1082     @VisibleForTesting
   1083     String getCallingPackage_() {
   1084         return getCallingPackage();
   1085     }
   1086 
   1087     @VisibleForTesting
   1088     void setOpenHelper(DatabaseHelper helper) {
   1089         mOpenHelper = helper;
   1090     }
   1091 
   1092     @Override
   1093     public String getType(Uri uri) {
   1094         switch (sUriMatcher.match(uri)) {
   1095             case MATCH_CHANNEL:
   1096                 return Channels.CONTENT_TYPE;
   1097             case MATCH_CHANNEL_ID:
   1098                 return Channels.CONTENT_ITEM_TYPE;
   1099             case MATCH_CHANNEL_ID_LOGO:
   1100                 return "image/png";
   1101             case MATCH_PASSTHROUGH_ID:
   1102                 return Channels.CONTENT_ITEM_TYPE;
   1103             case MATCH_PROGRAM:
   1104                 return Programs.CONTENT_TYPE;
   1105             case MATCH_PROGRAM_ID:
   1106                 return Programs.CONTENT_ITEM_TYPE;
   1107             case MATCH_WATCHED_PROGRAM:
   1108                 return WatchedPrograms.CONTENT_TYPE;
   1109             case MATCH_WATCHED_PROGRAM_ID:
   1110                 return WatchedPrograms.CONTENT_ITEM_TYPE;
   1111             case MATCH_RECORDED_PROGRAM:
   1112                 return RecordedPrograms.CONTENT_TYPE;
   1113             case MATCH_RECORDED_PROGRAM_ID:
   1114                 return RecordedPrograms.CONTENT_ITEM_TYPE;
   1115             case MATCH_PREVIEW_PROGRAM:
   1116                 return PreviewPrograms.CONTENT_TYPE;
   1117             case MATCH_PREVIEW_PROGRAM_ID:
   1118                 return PreviewPrograms.CONTENT_ITEM_TYPE;
   1119             case MATCH_WATCH_NEXT_PROGRAM:
   1120                 return WatchNextPrograms.CONTENT_TYPE;
   1121             case MATCH_WATCH_NEXT_PROGRAM_ID:
   1122                 return WatchNextPrograms.CONTENT_ITEM_TYPE;
   1123             default:
   1124                 throw new IllegalArgumentException("Unknown URI " + uri);
   1125         }
   1126     }
   1127 
   1128     @Override
   1129     public Bundle call(String method, String arg, Bundle extras) {
   1130         if (!callerHasAccessAllEpgDataPermission()) {
   1131             return null;
   1132         }
   1133         ensureInitialized();
   1134         Map<String, String> projectionMap;
   1135         switch (method) {
   1136             case TvContract.METHOD_GET_COLUMNS:
   1137                 switch (sUriMatcher.match(Uri.parse(arg))) {
   1138                     case MATCH_CHANNEL:
   1139                         projectionMap = sChannelProjectionMap;
   1140                         break;
   1141                     case MATCH_PROGRAM:
   1142                         projectionMap = sProgramProjectionMap;
   1143                         break;
   1144                     case MATCH_PREVIEW_PROGRAM:
   1145                         projectionMap = sPreviewProgramProjectionMap;
   1146                         break;
   1147                     case MATCH_WATCH_NEXT_PROGRAM:
   1148                         projectionMap = sWatchNextProgramProjectionMap;
   1149                         break;
   1150                     case MATCH_RECORDED_PROGRAM:
   1151                         projectionMap = sRecordedProgramProjectionMap;
   1152                         break;
   1153                     default:
   1154                         return null;
   1155                 }
   1156                 Bundle result = new Bundle();
   1157                 result.putStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES,
   1158                         projectionMap.keySet().toArray(new String[projectionMap.size()]));
   1159                 return result;
   1160             case TvContract.METHOD_ADD_COLUMN:
   1161                 CharSequence columnName = extras.getCharSequence(TvContract.EXTRA_COLUMN_NAME);
   1162                 CharSequence dataType = extras.getCharSequence(TvContract.EXTRA_DATA_TYPE);
   1163                 if (TextUtils.isEmpty(columnName) || TextUtils.isEmpty(dataType)) {
   1164                     return null;
   1165                 }
   1166                 CharSequence defaultValue = extras.getCharSequence(TvContract.EXTRA_DEFAULT_VALUE);
   1167                 try {
   1168                     defaultValue = TextUtils.isEmpty(defaultValue) ? "" : generateDefaultClause(
   1169                             dataType.toString(), defaultValue.toString());
   1170                 } catch (IllegalArgumentException e) {
   1171                     return null;
   1172                 }
   1173                 String tableName;
   1174                 switch (sUriMatcher.match(Uri.parse(arg))) {
   1175                     case MATCH_CHANNEL:
   1176                         tableName = CHANNELS_TABLE;
   1177                         projectionMap = sChannelProjectionMap;
   1178                         break;
   1179                     case MATCH_PROGRAM:
   1180                         tableName = PROGRAMS_TABLE;
   1181                         projectionMap = sProgramProjectionMap;
   1182                         break;
   1183                     case MATCH_PREVIEW_PROGRAM:
   1184                         tableName = PREVIEW_PROGRAMS_TABLE;
   1185                         projectionMap = sPreviewProgramProjectionMap;
   1186                         break;
   1187                     case MATCH_WATCH_NEXT_PROGRAM:
   1188                         tableName = WATCH_NEXT_PROGRAMS_TABLE;
   1189                         projectionMap = sWatchNextProgramProjectionMap;
   1190                         break;
   1191                     case MATCH_RECORDED_PROGRAM:
   1192                         tableName = RECORDED_PROGRAMS_TABLE;
   1193                         projectionMap = sRecordedProgramProjectionMap;
   1194                         break;
   1195                     default:
   1196                         return null;
   1197                 }
   1198                 try (SQLiteDatabase db = mOpenHelper.getWritableDatabase()) {
   1199                     db.execSQL("ALTER TABLE " + tableName + " ADD "
   1200                             + columnName + " " + dataType + defaultValue + ";");
   1201                     projectionMap.put(columnName.toString(), tableName + '.' + columnName);
   1202                     Bundle returnValue = new Bundle();
   1203                     returnValue.putStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES,
   1204                             projectionMap.keySet().toArray(new String[projectionMap.size()]));
   1205                     return returnValue;
   1206                 } catch (SQLException e) {
   1207                     return null;
   1208                 }
   1209             case TvContract.METHOD_GET_BLOCKED_PACKAGES:
   1210                 Bundle allBlockedPackages = new Bundle();
   1211                 allBlockedPackages.putStringArray(TvContract.EXTRA_BLOCKED_PACKAGES,
   1212                         sBlockedPackages.keySet().toArray(new String[sBlockedPackages.size()]));
   1213                 return allBlockedPackages;
   1214             case TvContract.METHOD_BLOCK_PACKAGE:
   1215                 String packageNameToBlock = arg;
   1216                 Bundle blockPackageResult = new Bundle();
   1217                 if (!TextUtils.isEmpty(packageNameToBlock)) {
   1218                     sBlockedPackages.put(packageNameToBlock, true);
   1219                     if (sBlockedPackagesSharedPreference.edit().putStringSet(
   1220                             SHARED_PREF_BLOCKED_PACKAGES_KEY, sBlockedPackages.keySet()).commit()) {
   1221                         String[] channelSelectionArgs = new String[] {
   1222                                 packageNameToBlock, Channels.TYPE_PREVIEW };
   1223                         delete(TvContract.Channels.CONTENT_URI,
   1224                                 Channels.COLUMN_PACKAGE_NAME + "=? AND "
   1225                                         + Channels.COLUMN_TYPE + "=?",
   1226                                 channelSelectionArgs);
   1227                         String[] programsSelectionArgs = new String[] {
   1228                                 packageNameToBlock };
   1229                         delete(TvContract.PreviewPrograms.CONTENT_URI,
   1230                                 PreviewPrograms.COLUMN_PACKAGE_NAME + "=?", programsSelectionArgs);
   1231                         delete(TvContract.WatchNextPrograms.CONTENT_URI,
   1232                                 WatchNextPrograms.COLUMN_PACKAGE_NAME + "=?",
   1233                                 programsSelectionArgs);
   1234                         blockPackageResult.putInt(
   1235                                 TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_OK);
   1236                     } else {
   1237                         Log.e(TAG, "Blocking package " + packageNameToBlock + " failed");
   1238                         sBlockedPackages.remove(packageNameToBlock);
   1239                         blockPackageResult.putInt(TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_ERROR_IO);
   1240                     }
   1241                 } else {
   1242                     blockPackageResult.putInt(
   1243                             TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_ERROR_INVALID_ARGUMENT);
   1244                 }
   1245                 return blockPackageResult;
   1246             case TvContract.METHOD_UNBLOCK_PACKAGE:
   1247                 String packageNameToUnblock = arg;
   1248                 Bundle unblockPackageResult = new Bundle();
   1249                 if (!TextUtils.isEmpty(packageNameToUnblock)) {
   1250                     sBlockedPackages.remove(packageNameToUnblock);
   1251                     if (sBlockedPackagesSharedPreference.edit().putStringSet(
   1252                             SHARED_PREF_BLOCKED_PACKAGES_KEY, sBlockedPackages.keySet()).commit()) {
   1253                         unblockPackageResult.putInt(
   1254                                 TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_OK);
   1255                     } else {
   1256                         Log.e(TAG, "Unblocking package " + packageNameToUnblock + " failed");
   1257                         sBlockedPackages.put(packageNameToUnblock, true);
   1258                         unblockPackageResult.putInt(
   1259                                 TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_ERROR_IO);
   1260                     }
   1261                 } else {
   1262                     unblockPackageResult.putInt(
   1263                             TvContract.EXTRA_RESULT_CODE, TvContract.RESULT_ERROR_INVALID_ARGUMENT);
   1264                 }
   1265                 return unblockPackageResult;
   1266         }
   1267         return null;
   1268     }
   1269 
   1270     @Override
   1271     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
   1272             String sortOrder) {
   1273         ensureInitialized();
   1274         mTransientRowHelper.ensureOldTransientRowsDeleted();
   1275         boolean needsToValidateSortOrder = !callerHasAccessAllEpgDataPermission();
   1276         SqlParams params = createSqlParams(OP_QUERY, uri, selection, selectionArgs);
   1277 
   1278         SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
   1279         queryBuilder.setStrict(needsToValidateSortOrder);
   1280         queryBuilder.setTables(params.getTables());
   1281         String orderBy = null;
   1282         Map<String, String> projectionMap;
   1283         switch (params.getTables()) {
   1284             case PROGRAMS_TABLE:
   1285                 projectionMap = sProgramProjectionMap;
   1286                 orderBy = DEFAULT_PROGRAMS_SORT_ORDER;
   1287                 break;
   1288             case WATCHED_PROGRAMS_TABLE:
   1289                 projectionMap = sWatchedProgramProjectionMap;
   1290                 orderBy = DEFAULT_WATCHED_PROGRAMS_SORT_ORDER;
   1291                 break;
   1292             case RECORDED_PROGRAMS_TABLE:
   1293                 projectionMap = sRecordedProgramProjectionMap;
   1294                 break;
   1295             case PREVIEW_PROGRAMS_TABLE:
   1296                 projectionMap = sPreviewProgramProjectionMap;
   1297                 break;
   1298             case WATCH_NEXT_PROGRAMS_TABLE:
   1299                 projectionMap = sWatchNextProgramProjectionMap;
   1300                 break;
   1301             default:
   1302                 projectionMap = sChannelProjectionMap;
   1303                 break;
   1304         }
   1305         queryBuilder.setProjectionMap(createProjectionMapForQuery(projection, projectionMap));
   1306         if (needsToValidateSortOrder) {
   1307             validateSortOrder(sortOrder, projectionMap.keySet());
   1308         }
   1309 
   1310         // Use the default sort order only if no sort order is specified.
   1311         if (!TextUtils.isEmpty(sortOrder)) {
   1312             orderBy = sortOrder;
   1313         }
   1314 
   1315         // Get the database and run the query.
   1316         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
   1317         Cursor c = queryBuilder.query(db, projection, params.getSelection(),
   1318                 params.getSelectionArgs(), null, null, orderBy);
   1319 
   1320         // Tell the cursor what URI to watch, so it knows when its source data changes.
   1321         c.setNotificationUri(getContext().getContentResolver(), uri);
   1322         return c;
   1323     }
   1324 
   1325     @Override
   1326     public Uri insert(Uri uri, ContentValues values) {
   1327         ensureInitialized();
   1328         mTransientRowHelper.ensureOldTransientRowsDeleted();
   1329         switch (sUriMatcher.match(uri)) {
   1330             case MATCH_CHANNEL:
   1331                 // Preview channels are not necessarily associated with TV input service.
   1332                 // Therefore, we fill a fake ID to meet not null restriction for preview channels.
   1333                 if (values.get(Channels.COLUMN_INPUT_ID) == null
   1334                         && Channels.TYPE_PREVIEW.equals(values.get(Channels.COLUMN_TYPE))) {
   1335                     values.put(Channels.COLUMN_INPUT_ID, EMPTY_STRING);
   1336                 }
   1337                 filterContentValues(values, sChannelProjectionMap);
   1338                 return insertChannel(uri, values);
   1339             case MATCH_PROGRAM:
   1340                 filterContentValues(values, sProgramProjectionMap);
   1341                 return insertProgram(uri, values);
   1342             case MATCH_WATCHED_PROGRAM:
   1343                 return insertWatchedProgram(uri, values);
   1344             case MATCH_RECORDED_PROGRAM:
   1345                 filterContentValues(values, sRecordedProgramProjectionMap);
   1346                 return insertRecordedProgram(uri, values);
   1347             case MATCH_PREVIEW_PROGRAM:
   1348                 filterContentValues(values, sPreviewProgramProjectionMap);
   1349                 return insertPreviewProgram(uri, values);
   1350             case MATCH_WATCH_NEXT_PROGRAM:
   1351                 filterContentValues(values, sWatchNextProgramProjectionMap);
   1352                 return insertWatchNextProgram(uri, values);
   1353             case MATCH_CHANNEL_ID:
   1354             case MATCH_CHANNEL_ID_LOGO:
   1355             case MATCH_PASSTHROUGH_ID:
   1356             case MATCH_PROGRAM_ID:
   1357             case MATCH_WATCHED_PROGRAM_ID:
   1358             case MATCH_RECORDED_PROGRAM_ID:
   1359             case MATCH_PREVIEW_PROGRAM_ID:
   1360                 throw new UnsupportedOperationException("Cannot insert into that URI: " + uri);
   1361             default:
   1362                 throw new IllegalArgumentException("Unknown URI " + uri);
   1363         }
   1364     }
   1365 
   1366     private Uri insertChannel(Uri uri, ContentValues values) {
   1367         if (TextUtils.equals(values.getAsString(Channels.COLUMN_TYPE), Channels.TYPE_PREVIEW)) {
   1368             blockIllegalAccessFromBlockedPackage();
   1369         }
   1370         // Mark the owner package of this channel.
   1371         values.put(Channels.COLUMN_PACKAGE_NAME, getCallingPackage_());
   1372         blockIllegalAccessToChannelsSystemColumns(values);
   1373 
   1374         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1375         long rowId = db.insert(CHANNELS_TABLE, null, values);
   1376         if (rowId > 0) {
   1377             Uri channelUri = TvContract.buildChannelUri(rowId);
   1378             notifyChange(channelUri);
   1379             return channelUri;
   1380         }
   1381 
   1382         throw new SQLException("Failed to insert row into " + uri);
   1383     }
   1384 
   1385     private Uri insertProgram(Uri uri, ContentValues values) {
   1386         // Mark the owner package of this program.
   1387         values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());
   1388 
   1389         checkAndConvertGenre(values);
   1390         checkAndConvertDeprecatedColumns(values);
   1391 
   1392         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1393         long rowId = db.insert(PROGRAMS_TABLE, null, values);
   1394         if (rowId > 0) {
   1395             Uri programUri = TvContract.buildProgramUri(rowId);
   1396             notifyChange(programUri);
   1397             return programUri;
   1398         }
   1399 
   1400         throw new SQLException("Failed to insert row into " + uri);
   1401     }
   1402 
   1403     private Uri insertWatchedProgram(Uri uri, ContentValues values) {
   1404         if (DEBUG) {
   1405             Log.d(TAG, "insertWatchedProgram(uri=" + uri + ", values={" + values + "})");
   1406         }
   1407         Long watchStartTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS);
   1408         Long watchEndTime = values.getAsLong(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS);
   1409         // The system sends only two kinds of watch events:
   1410         // 1. The user tunes to a new channel. (COLUMN_WATCH_START_TIME_UTC_MILLIS)
   1411         // 2. The user stops watching. (COLUMN_WATCH_END_TIME_UTC_MILLIS)
   1412         if (watchStartTime != null && watchEndTime == null) {
   1413             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1414             long rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values);
   1415             if (rowId > 0) {
   1416                 mLogHandler.removeMessages(WatchLogHandler.MSG_TRY_CONSOLIDATE_ALL);
   1417                 mLogHandler.sendEmptyMessageDelayed(WatchLogHandler.MSG_TRY_CONSOLIDATE_ALL,
   1418                         MAX_PROGRAM_DATA_DELAY_IN_MILLIS);
   1419                 return TvContract.buildWatchedProgramUri(rowId);
   1420             }
   1421             Log.w(TAG, "Failed to insert row for " + values + ". Channel does not exist.");
   1422             return null;
   1423         } else if (watchStartTime == null && watchEndTime != null) {
   1424             SomeArgs args = SomeArgs.obtain();
   1425             args.arg1 = values.getAsString(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN);
   1426             args.arg2 = watchEndTime;
   1427             Message msg = mLogHandler.obtainMessage(WatchLogHandler.MSG_CONSOLIDATE, args);
   1428             mLogHandler.sendMessageDelayed(msg, MAX_PROGRAM_DATA_DELAY_IN_MILLIS);
   1429             return null;
   1430         }
   1431         // All the other cases are invalid.
   1432         throw new IllegalArgumentException("Only one of COLUMN_WATCH_START_TIME_UTC_MILLIS and"
   1433                 + " COLUMN_WATCH_END_TIME_UTC_MILLIS should be specified");
   1434     }
   1435 
   1436     private Uri insertRecordedProgram(Uri uri, ContentValues values) {
   1437         // Mark the owner package of this program.
   1438         values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());
   1439 
   1440         checkAndConvertGenre(values);
   1441 
   1442         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1443         long rowId = db.insert(RECORDED_PROGRAMS_TABLE, null, values);
   1444         if (rowId > 0) {
   1445             Uri recordedProgramUri = TvContract.buildRecordedProgramUri(rowId);
   1446             notifyChange(recordedProgramUri);
   1447             return recordedProgramUri;
   1448         }
   1449 
   1450         throw new SQLException("Failed to insert row into " + uri);
   1451     }
   1452 
   1453     private Uri insertPreviewProgram(Uri uri, ContentValues values) {
   1454         if (!values.containsKey(PreviewPrograms.COLUMN_TYPE)) {
   1455             throw new IllegalArgumentException("Missing the required column: " +
   1456                     PreviewPrograms.COLUMN_TYPE);
   1457         }
   1458         blockIllegalAccessFromBlockedPackage();
   1459         // Mark the owner package of this program.
   1460         values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());
   1461         blockIllegalAccessToPreviewProgramsSystemColumns(values);
   1462 
   1463         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1464         long rowId = db.insert(PREVIEW_PROGRAMS_TABLE, null, values);
   1465         if (rowId > 0) {
   1466             Uri previewProgramUri = TvContract.buildPreviewProgramUri(rowId);
   1467             notifyChange(previewProgramUri);
   1468             return previewProgramUri;
   1469         }
   1470 
   1471         throw new SQLException("Failed to insert row into " + uri);
   1472     }
   1473 
   1474     private Uri insertWatchNextProgram(Uri uri, ContentValues values) {
   1475         if (!values.containsKey(WatchNextPrograms.COLUMN_TYPE)) {
   1476             throw new IllegalArgumentException("Missing the required column: " +
   1477                     WatchNextPrograms.COLUMN_TYPE);
   1478         }
   1479         blockIllegalAccessFromBlockedPackage();
   1480         if (!callerHasAccessAllEpgDataPermission() ||
   1481                 !values.containsKey(Programs.COLUMN_PACKAGE_NAME)) {
   1482             // Mark the owner package of this program. System app with a proper permission may
   1483             // change the owner of the program.
   1484             values.put(Programs.COLUMN_PACKAGE_NAME, getCallingPackage_());
   1485         }
   1486         blockIllegalAccessToPreviewProgramsSystemColumns(values);
   1487 
   1488         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1489         long rowId = db.insert(WATCH_NEXT_PROGRAMS_TABLE, null, values);
   1490         if (rowId > 0) {
   1491             Uri watchNextProgramUri = TvContract.buildWatchNextProgramUri(rowId);
   1492             notifyChange(watchNextProgramUri);
   1493             return watchNextProgramUri;
   1494         }
   1495 
   1496         throw new SQLException("Failed to insert row into " + uri);
   1497     }
   1498 
   1499     @Override
   1500     public int delete(Uri uri, String selection, String[] selectionArgs) {
   1501         mTransientRowHelper.ensureOldTransientRowsDeleted();
   1502         SqlParams params = createSqlParams(OP_DELETE, uri, selection, selectionArgs);
   1503         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1504         int count;
   1505         switch (sUriMatcher.match(uri)) {
   1506             case MATCH_CHANNEL_ID_LOGO:
   1507                 ContentValues values = new ContentValues();
   1508                 values.putNull(CHANNELS_COLUMN_LOGO);
   1509                 count = db.update(params.getTables(), values, params.getSelection(),
   1510                         params.getSelectionArgs());
   1511                 break;
   1512             case MATCH_CHANNEL:
   1513             case MATCH_PROGRAM:
   1514             case MATCH_WATCHED_PROGRAM:
   1515             case MATCH_RECORDED_PROGRAM:
   1516             case MATCH_PREVIEW_PROGRAM:
   1517             case MATCH_WATCH_NEXT_PROGRAM:
   1518             case MATCH_CHANNEL_ID:
   1519             case MATCH_PASSTHROUGH_ID:
   1520             case MATCH_PROGRAM_ID:
   1521             case MATCH_WATCHED_PROGRAM_ID:
   1522             case MATCH_RECORDED_PROGRAM_ID:
   1523             case MATCH_PREVIEW_PROGRAM_ID:
   1524             case MATCH_WATCH_NEXT_PROGRAM_ID:
   1525                 count = db.delete(params.getTables(), params.getSelection(),
   1526                         params.getSelectionArgs());
   1527                 break;
   1528             default:
   1529                 throw new IllegalArgumentException("Unknown URI " + uri);
   1530         }
   1531         if (count > 0) {
   1532             notifyChange(uri);
   1533         }
   1534         return count;
   1535     }
   1536 
   1537     @Override
   1538     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
   1539         mTransientRowHelper.ensureOldTransientRowsDeleted();
   1540         SqlParams params = createSqlParams(OP_UPDATE, uri, selection, selectionArgs);
   1541         blockIllegalAccessToIdAndPackageName(uri, values);
   1542         boolean containImmutableColumn = false;
   1543         if (params.getTables().equals(CHANNELS_TABLE)) {
   1544             filterContentValues(values, sChannelProjectionMap);
   1545             containImmutableColumn = disallowModifyChannelType(values, params);
   1546             if (containImmutableColumn && sUriMatcher.match(uri) != MATCH_CHANNEL_ID) {
   1547                 Log.i(TAG, "Updating failed. Attempt to change immutable column for channels.");
   1548                 return 0;
   1549             }
   1550             blockIllegalAccessToChannelsSystemColumns(values);
   1551         } else if (params.getTables().equals(PROGRAMS_TABLE)) {
   1552             filterContentValues(values, sProgramProjectionMap);
   1553             checkAndConvertGenre(values);
   1554             checkAndConvertDeprecatedColumns(values);
   1555         } else if (params.getTables().equals(RECORDED_PROGRAMS_TABLE)) {
   1556             filterContentValues(values, sRecordedProgramProjectionMap);
   1557             checkAndConvertGenre(values);
   1558         } else if (params.getTables().equals(PREVIEW_PROGRAMS_TABLE)) {
   1559             filterContentValues(values, sPreviewProgramProjectionMap);
   1560             containImmutableColumn = disallowModifyChannelId(values, params);
   1561             if (containImmutableColumn && PreviewPrograms.CONTENT_URI.equals(uri)) {
   1562                 Log.i(TAG, "Updating failed. Attempt to change unmodifiable column for "
   1563                         + "preview programs.");
   1564                 return 0;
   1565             }
   1566             blockIllegalAccessToPreviewProgramsSystemColumns(values);
   1567         } else if (params.getTables().equals(WATCH_NEXT_PROGRAMS_TABLE)) {
   1568             filterContentValues(values, sWatchNextProgramProjectionMap);
   1569             blockIllegalAccessToPreviewProgramsSystemColumns(values);
   1570         }
   1571         if (values.size() == 0) {
   1572             // All values may be filtered out, no need to update
   1573             return 0;
   1574         }
   1575         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1576         int count = db.update(params.getTables(), values, params.getSelection(),
   1577                 params.getSelectionArgs());
   1578         if (count > 0) {
   1579             notifyChange(uri);
   1580         } else if (containImmutableColumn) {
   1581             Log.i(TAG, "Updating failed. The item may not exist or attempt to change "
   1582                     + "immutable column.");
   1583         }
   1584         return count;
   1585     }
   1586 
   1587     private synchronized void ensureInitialized() {
   1588         if (!sInitialized) {
   1589             // Database is not accessed before and the projection maps and the blocked package list
   1590             // are not updated yet. Gets database here to make it initialized.
   1591             mOpenHelper.getReadableDatabase();
   1592         }
   1593     }
   1594 
   1595     private Map<String, String> createProjectionMapForQuery(String[] projection,
   1596             Map<String, String> projectionMap) {
   1597         if (projection == null) {
   1598             return projectionMap;
   1599         }
   1600         Map<String, String> columnProjectionMap = new HashMap<>();
   1601         for (String columnName : projection) {
   1602             // Value NULL will be provided if the requested column does not exist in the database.
   1603             columnProjectionMap.put(columnName,
   1604                     projectionMap.getOrDefault(columnName, "NULL as " + columnName));
   1605         }
   1606         return columnProjectionMap;
   1607     }
   1608 
   1609     private void filterContentValues(ContentValues values, Map<String, String> projectionMap) {
   1610         Iterator<String> iter = values.keySet().iterator();
   1611         while (iter.hasNext()) {
   1612             String columnName = iter.next();
   1613             if (!projectionMap.containsKey(columnName)) {
   1614                 iter.remove();
   1615             }
   1616         }
   1617     }
   1618 
   1619     private SqlParams createSqlParams(String operation, Uri uri, String selection,
   1620             String[] selectionArgs) {
   1621         int match = sUriMatcher.match(uri);
   1622         SqlParams params = new SqlParams(null, selection, selectionArgs);
   1623 
   1624         // Control access to EPG data (excluding watched programs) when the caller doesn't have all
   1625         // access.
   1626         String prefix = match == MATCH_CHANNEL ? CHANNELS_TABLE + "." : "";
   1627         if (!callerHasAccessAllEpgDataPermission()
   1628                 && match != MATCH_WATCHED_PROGRAM && match != MATCH_WATCHED_PROGRAM_ID) {
   1629             if (!TextUtils.isEmpty(selection)) {
   1630                 throw new SecurityException("Selection not allowed for " + uri);
   1631             }
   1632             // Limit the operation only to the data that the calling package owns except for when
   1633             // the caller tries to read TV listings and has the appropriate permission.
   1634             if (operation.equals(OP_QUERY) && callerHasReadTvListingsPermission()) {
   1635                 params.setWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=? OR "
   1636                         + Channels.COLUMN_SEARCHABLE + "=?", getCallingPackage_(), "1");
   1637             } else {
   1638                 params.setWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=?",
   1639                         getCallingPackage_());
   1640             }
   1641         }
   1642         String packageName = uri.getQueryParameter(TvContract.PARAM_PACKAGE);
   1643         if (packageName != null) {
   1644             params.appendWhere(prefix + BaseTvColumns.COLUMN_PACKAGE_NAME + "=?", packageName);
   1645         }
   1646 
   1647         switch (match) {
   1648             case MATCH_CHANNEL:
   1649                 String genre = uri.getQueryParameter(TvContract.PARAM_CANONICAL_GENRE);
   1650                 if (genre == null) {
   1651                     params.setTables(CHANNELS_TABLE);
   1652                 } else {
   1653                     if (!operation.equals(OP_QUERY)) {
   1654                         throw new SecurityException(capitalize(operation)
   1655                                 + " not allowed for " + uri);
   1656                     }
   1657                     if (!Genres.isCanonical(genre)) {
   1658                         throw new IllegalArgumentException("Not a canonical genre : " + genre);
   1659                     }
   1660                     params.setTables(CHANNELS_TABLE_INNER_JOIN_PROGRAMS_TABLE);
   1661                     String curTime = String.valueOf(System.currentTimeMillis());
   1662                     params.appendWhere("LIKE(?, " + Programs.COLUMN_CANONICAL_GENRE + ") AND "
   1663                             + Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND "
   1664                             + Programs.COLUMN_END_TIME_UTC_MILLIS + ">=?",
   1665                             "%" + genre + "%", curTime, curTime);
   1666                 }
   1667                 String inputId = uri.getQueryParameter(TvContract.PARAM_INPUT);
   1668                 if (inputId != null) {
   1669                     params.appendWhere(Channels.COLUMN_INPUT_ID + "=?", inputId);
   1670                 }
   1671                 boolean browsableOnly = uri.getBooleanQueryParameter(
   1672                         TvContract.PARAM_BROWSABLE_ONLY, false);
   1673                 if (browsableOnly) {
   1674                     params.appendWhere(Channels.COLUMN_BROWSABLE + "=1");
   1675                 }
   1676                 String preview = uri.getQueryParameter(TvContract.PARAM_PREVIEW);
   1677                 if (preview != null) {
   1678                     String previewSelection = Channels.COLUMN_TYPE
   1679                             + (preview.equals(String.valueOf(true)) ? "=?" : "!=?");
   1680                     params.appendWhere(previewSelection, Channels.TYPE_PREVIEW);
   1681                 }
   1682                 break;
   1683             case MATCH_CHANNEL_ID:
   1684                 params.setTables(CHANNELS_TABLE);
   1685                 params.appendWhere(Channels._ID + "=?", uri.getLastPathSegment());
   1686                 break;
   1687             case MATCH_PROGRAM:
   1688                 params.setTables(PROGRAMS_TABLE);
   1689                 String paramChannelId = uri.getQueryParameter(TvContract.PARAM_CHANNEL);
   1690                 if (paramChannelId != null) {
   1691                     String channelId = String.valueOf(Long.parseLong(paramChannelId));
   1692                     params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId);
   1693                 }
   1694                 String paramStartTime = uri.getQueryParameter(TvContract.PARAM_START_TIME);
   1695                 String paramEndTime = uri.getQueryParameter(TvContract.PARAM_END_TIME);
   1696                 if (paramStartTime != null && paramEndTime != null) {
   1697                     String startTime = String.valueOf(Long.parseLong(paramStartTime));
   1698                     String endTime = String.valueOf(Long.parseLong(paramEndTime));
   1699                     params.appendWhere(Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND "
   1700                             + Programs.COLUMN_END_TIME_UTC_MILLIS + ">=?", endTime, startTime);
   1701                 }
   1702                 break;
   1703             case MATCH_PROGRAM_ID:
   1704                 params.setTables(PROGRAMS_TABLE);
   1705                 params.appendWhere(Programs._ID + "=?", uri.getLastPathSegment());
   1706                 break;
   1707             case MATCH_WATCHED_PROGRAM:
   1708                 if (!callerHasAccessWatchedProgramsPermission()) {
   1709                     throw new SecurityException("Access not allowed for " + uri);
   1710                 }
   1711                 params.setTables(WATCHED_PROGRAMS_TABLE);
   1712                 params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1");
   1713                 break;
   1714             case MATCH_WATCHED_PROGRAM_ID:
   1715                 if (!callerHasAccessWatchedProgramsPermission()) {
   1716                     throw new SecurityException("Access not allowed for " + uri);
   1717                 }
   1718                 params.setTables(WATCHED_PROGRAMS_TABLE);
   1719                 params.appendWhere(WatchedPrograms._ID + "=?", uri.getLastPathSegment());
   1720                 params.appendWhere(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=?", "1");
   1721                 break;
   1722             case MATCH_RECORDED_PROGRAM_ID:
   1723                 params.appendWhere(RecordedPrograms._ID + "=?", uri.getLastPathSegment());
   1724                 // fall-through
   1725             case MATCH_RECORDED_PROGRAM:
   1726                 params.setTables(RECORDED_PROGRAMS_TABLE);
   1727                 paramChannelId = uri.getQueryParameter(TvContract.PARAM_CHANNEL);
   1728                 if (paramChannelId != null) {
   1729                     String channelId = String.valueOf(Long.parseLong(paramChannelId));
   1730                     params.appendWhere(Programs.COLUMN_CHANNEL_ID + "=?", channelId);
   1731                 }
   1732                 break;
   1733             case MATCH_PREVIEW_PROGRAM_ID:
   1734                 params.appendWhere(PreviewPrograms._ID + "=?", uri.getLastPathSegment());
   1735                 // fall-through
   1736             case MATCH_PREVIEW_PROGRAM:
   1737                 params.setTables(PREVIEW_PROGRAMS_TABLE);
   1738                 paramChannelId = uri.getQueryParameter(TvContract.PARAM_CHANNEL);
   1739                 if (paramChannelId != null) {
   1740                     String channelId = String.valueOf(Long.parseLong(paramChannelId));
   1741                     params.appendWhere(PreviewPrograms.COLUMN_CHANNEL_ID + "=?", channelId);
   1742                 }
   1743                 break;
   1744             case MATCH_WATCH_NEXT_PROGRAM_ID:
   1745                 params.appendWhere(WatchNextPrograms._ID + "=?", uri.getLastPathSegment());
   1746                 // fall-through
   1747             case MATCH_WATCH_NEXT_PROGRAM:
   1748                 params.setTables(WATCH_NEXT_PROGRAMS_TABLE);
   1749                 break;
   1750             case MATCH_CHANNEL_ID_LOGO:
   1751                 if (operation.equals(OP_DELETE)) {
   1752                     params.setTables(CHANNELS_TABLE);
   1753                     params.appendWhere(Channels._ID + "=?", uri.getPathSegments().get(1));
   1754                     break;
   1755                 }
   1756                 // fall-through
   1757             case MATCH_PASSTHROUGH_ID:
   1758                 throw new UnsupportedOperationException(operation + " not permmitted on " + uri);
   1759             default:
   1760                 throw new IllegalArgumentException("Unknown URI " + uri);
   1761         }
   1762         return params;
   1763     }
   1764 
   1765     private static String generateDefaultClause(String dataType, String defaultValue)
   1766             throws IllegalArgumentException {
   1767         String defaultValueString = " DEFAULT ";
   1768         switch (dataType.toLowerCase()) {
   1769             case "integer":
   1770                 return defaultValueString + Integer.parseInt(defaultValue);
   1771             case "real":
   1772                 return defaultValueString + Double.parseDouble(defaultValue);
   1773             case "text":
   1774             case "blob":
   1775                 return defaultValueString + DatabaseUtils.sqlEscapeString(defaultValue);
   1776             default:
   1777                 throw new IllegalArgumentException("Illegal data type \"" + dataType
   1778                         + "\" with default value: " + defaultValue);
   1779         }
   1780     }
   1781 
   1782     private static String capitalize(String str) {
   1783         return Character.toUpperCase(str.charAt(0)) + str.substring(1);
   1784     }
   1785 
   1786     @SuppressLint("DefaultLocale")
   1787     private void checkAndConvertGenre(ContentValues values) {
   1788         String canonicalGenres = values.getAsString(Programs.COLUMN_CANONICAL_GENRE);
   1789 
   1790         if (!TextUtils.isEmpty(canonicalGenres)) {
   1791             // Check if the canonical genres are valid. If not, clear them.
   1792             String[] genres = Genres.decode(canonicalGenres);
   1793             for (String genre : genres) {
   1794                 if (!Genres.isCanonical(genre)) {
   1795                     values.putNull(Programs.COLUMN_CANONICAL_GENRE);
   1796                     canonicalGenres = null;
   1797                     break;
   1798                 }
   1799             }
   1800         }
   1801 
   1802         if (TextUtils.isEmpty(canonicalGenres)) {
   1803             // If the canonical genre is not set, try to map the broadcast genre to the canonical
   1804             // genre.
   1805             String broadcastGenres = values.getAsString(Programs.COLUMN_BROADCAST_GENRE);
   1806             if (!TextUtils.isEmpty(broadcastGenres)) {
   1807                 Set<String> genreSet = new HashSet<>();
   1808                 String[] genres = Genres.decode(broadcastGenres);
   1809                 for (String genre : genres) {
   1810                     String canonicalGenre = sGenreMap.get(genre.toUpperCase());
   1811                     if (Genres.isCanonical(canonicalGenre)) {
   1812                         genreSet.add(canonicalGenre);
   1813                     }
   1814                 }
   1815                 if (genreSet.size() > 0) {
   1816                     values.put(Programs.COLUMN_CANONICAL_GENRE,
   1817                             Genres.encode(genreSet.toArray(new String[genreSet.size()])));
   1818                 }
   1819             }
   1820         }
   1821     }
   1822 
   1823     private void checkAndConvertDeprecatedColumns(ContentValues values) {
   1824         if (values.containsKey(Programs.COLUMN_SEASON_NUMBER)) {
   1825             if (!values.containsKey(Programs.COLUMN_SEASON_DISPLAY_NUMBER)) {
   1826                 values.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER, values.getAsInteger(
   1827                         Programs.COLUMN_SEASON_NUMBER));
   1828             }
   1829             values.remove(Programs.COLUMN_SEASON_NUMBER);
   1830         }
   1831         if (values.containsKey(Programs.COLUMN_EPISODE_NUMBER)) {
   1832             if (!values.containsKey(Programs.COLUMN_EPISODE_DISPLAY_NUMBER)) {
   1833                 values.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER, values.getAsInteger(
   1834                         Programs.COLUMN_EPISODE_NUMBER));
   1835             }
   1836             values.remove(Programs.COLUMN_EPISODE_NUMBER);
   1837         }
   1838     }
   1839 
   1840     // We might have more than one thread trying to make its way through applyBatch() so the
   1841     // notification coalescing needs to be thread-local to work correctly.
   1842     private final ThreadLocal<Set<Uri>> mTLBatchNotifications = new ThreadLocal<>();
   1843 
   1844     private Set<Uri> getBatchNotificationsSet() {
   1845         return mTLBatchNotifications.get();
   1846     }
   1847 
   1848     private void setBatchNotificationsSet(Set<Uri> batchNotifications) {
   1849         mTLBatchNotifications.set(batchNotifications);
   1850     }
   1851 
   1852     @Override
   1853     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
   1854             throws OperationApplicationException {
   1855         setBatchNotificationsSet(new HashSet<Uri>());
   1856         Context context = getContext();
   1857         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1858         db.beginTransaction();
   1859         try {
   1860             ContentProviderResult[] results = super.applyBatch(operations);
   1861             db.setTransactionSuccessful();
   1862             return results;
   1863         } finally {
   1864             db.endTransaction();
   1865             final Set<Uri> notifications = getBatchNotificationsSet();
   1866             setBatchNotificationsSet(null);
   1867             for (final Uri uri : notifications) {
   1868                 context.getContentResolver().notifyChange(uri, null);
   1869             }
   1870         }
   1871     }
   1872 
   1873     @Override
   1874     public int bulkInsert(Uri uri, ContentValues[] values) {
   1875         setBatchNotificationsSet(new HashSet<Uri>());
   1876         Context context = getContext();
   1877         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   1878         db.beginTransaction();
   1879         try {
   1880             int result = super.bulkInsert(uri, values);
   1881             db.setTransactionSuccessful();
   1882             return result;
   1883         } finally {
   1884             db.endTransaction();
   1885             final Set<Uri> notifications = getBatchNotificationsSet();
   1886             setBatchNotificationsSet(null);
   1887             for (final Uri notificationUri : notifications) {
   1888                 context.getContentResolver().notifyChange(notificationUri, null);
   1889             }
   1890         }
   1891     }
   1892 
   1893     private void notifyChange(Uri uri) {
   1894         final Set<Uri> batchNotifications = getBatchNotificationsSet();
   1895         if (batchNotifications != null) {
   1896             batchNotifications.add(uri);
   1897         } else {
   1898             getContext().getContentResolver().notifyChange(uri, null);
   1899         }
   1900     }
   1901 
   1902     private boolean callerHasReadTvListingsPermission() {
   1903         return getContext().checkCallingOrSelfPermission(PERMISSION_READ_TV_LISTINGS)
   1904                 == PackageManager.PERMISSION_GRANTED;
   1905     }
   1906 
   1907     private boolean callerHasAccessAllEpgDataPermission() {
   1908         return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_ALL_EPG_DATA)
   1909                 == PackageManager.PERMISSION_GRANTED;
   1910     }
   1911 
   1912     private boolean callerHasAccessWatchedProgramsPermission() {
   1913         return getContext().checkCallingOrSelfPermission(PERMISSION_ACCESS_WATCHED_PROGRAMS)
   1914                 == PackageManager.PERMISSION_GRANTED;
   1915     }
   1916 
   1917     private boolean callerHasModifyParentalControlsPermission() {
   1918         return getContext().checkCallingOrSelfPermission(
   1919                 android.Manifest.permission.MODIFY_PARENTAL_CONTROLS)
   1920                 == PackageManager.PERMISSION_GRANTED;
   1921     }
   1922 
   1923     private void blockIllegalAccessToIdAndPackageName(Uri uri, ContentValues values) {
   1924         if (values.containsKey(BaseColumns._ID)) {
   1925             int match = sUriMatcher.match(uri);
   1926             switch (match) {
   1927                 case MATCH_CHANNEL_ID:
   1928                 case MATCH_PROGRAM_ID:
   1929                 case MATCH_PREVIEW_PROGRAM_ID:
   1930                 case MATCH_RECORDED_PROGRAM_ID:
   1931                 case MATCH_WATCH_NEXT_PROGRAM_ID:
   1932                 case MATCH_WATCHED_PROGRAM_ID:
   1933                     if (TextUtils.equals(values.getAsString(BaseColumns._ID),
   1934                             uri.getLastPathSegment())) {
   1935                         break;
   1936                     }
   1937                 default:
   1938                     throw new IllegalArgumentException("Not allowed to change ID.");
   1939             }
   1940         }
   1941         if (values.containsKey(BaseTvColumns.COLUMN_PACKAGE_NAME)
   1942                 && !callerHasAccessAllEpgDataPermission() && !TextUtils.equals(values.getAsString(
   1943                         BaseTvColumns.COLUMN_PACKAGE_NAME), getCallingPackage_())) {
   1944             throw new SecurityException("Not allowed to change package name.");
   1945         }
   1946     }
   1947 
   1948     private void blockIllegalAccessToChannelsSystemColumns(ContentValues values) {
   1949         if (values.containsKey(Channels.COLUMN_LOCKED)
   1950                 && !callerHasModifyParentalControlsPermission()) {
   1951             throw new SecurityException("Not allowed to access Channels.COLUMN_LOCKED");
   1952         }
   1953         Boolean hasAccessAllEpgDataPermission = null;
   1954         if (values.containsKey(Channels.COLUMN_BROWSABLE)) {
   1955             hasAccessAllEpgDataPermission = callerHasAccessAllEpgDataPermission();
   1956             if (!hasAccessAllEpgDataPermission) {
   1957                 throw new SecurityException("Not allowed to access Channels.COLUMN_BROWSABLE");
   1958             }
   1959         }
   1960     }
   1961 
   1962     private void blockIllegalAccessToPreviewProgramsSystemColumns(ContentValues values) {
   1963         if (values.containsKey(PreviewPrograms.COLUMN_BROWSABLE)
   1964                 && !callerHasAccessAllEpgDataPermission()) {
   1965             throw new SecurityException("Not allowed to access Programs.COLUMN_BROWSABLE");
   1966         }
   1967     }
   1968 
   1969     private void blockIllegalAccessFromBlockedPackage() {
   1970         String callingPackageName = getCallingPackage_();
   1971         if (sBlockedPackages.containsKey(callingPackageName)) {
   1972             throw new SecurityException(
   1973                     "Not allowed to access " + TvContract.AUTHORITY + ", "
   1974                     + callingPackageName + " is blocked");
   1975         }
   1976     }
   1977 
   1978     private boolean disallowModifyChannelType(ContentValues values, SqlParams params) {
   1979         if (values.containsKey(Channels.COLUMN_TYPE)) {
   1980             params.appendWhere(Channels.COLUMN_TYPE + "=?",
   1981                     values.getAsString(Channels.COLUMN_TYPE));
   1982             return true;
   1983         }
   1984         return false;
   1985     }
   1986 
   1987     private boolean disallowModifyChannelId(ContentValues values, SqlParams params) {
   1988         if (values.containsKey(PreviewPrograms.COLUMN_CHANNEL_ID)) {
   1989             params.appendWhere(PreviewPrograms.COLUMN_CHANNEL_ID + "=?",
   1990                     values.getAsString(PreviewPrograms.COLUMN_CHANNEL_ID));
   1991             return true;
   1992         }
   1993         return false;
   1994     }
   1995 
   1996     @Override
   1997     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
   1998         switch (sUriMatcher.match(uri)) {
   1999             case MATCH_CHANNEL_ID_LOGO:
   2000                 return openLogoFile(uri, mode);
   2001             default:
   2002                 throw new FileNotFoundException(uri.toString());
   2003         }
   2004     }
   2005 
   2006     private ParcelFileDescriptor openLogoFile(Uri uri, String mode) throws FileNotFoundException {
   2007         long channelId = Long.parseLong(uri.getPathSegments().get(1));
   2008 
   2009         SqlParams params = new SqlParams(CHANNELS_TABLE, Channels._ID + "=?",
   2010                 String.valueOf(channelId));
   2011         if (!callerHasAccessAllEpgDataPermission()) {
   2012             if (callerHasReadTvListingsPermission()) {
   2013                 params.appendWhere(Channels.COLUMN_PACKAGE_NAME + "=? OR "
   2014                         + Channels.COLUMN_SEARCHABLE + "=?", getCallingPackage_(), "1");
   2015             } else {
   2016                 params.appendWhere(Channels.COLUMN_PACKAGE_NAME + "=?", getCallingPackage_());
   2017             }
   2018         }
   2019 
   2020         SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
   2021         queryBuilder.setTables(params.getTables());
   2022 
   2023         // We don't write the database here.
   2024         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
   2025         if (mode.equals("r")) {
   2026             String sql = queryBuilder.buildQuery(new String[] { CHANNELS_COLUMN_LOGO },
   2027                     params.getSelection(), null, null, null, null);
   2028             ParcelFileDescriptor fd = DatabaseUtils.blobFileDescriptorForQuery(
   2029                     db, sql, params.getSelectionArgs());
   2030             if (fd == null) {
   2031                 throw new FileNotFoundException(uri.toString());
   2032             }
   2033             return fd;
   2034         } else {
   2035             try (Cursor cursor = queryBuilder.query(db, new String[] { Channels._ID },
   2036                     params.getSelection(), params.getSelectionArgs(), null, null, null)) {
   2037                 if (cursor.getCount() < 1) {
   2038                     // Fails early if corresponding channel does not exist.
   2039                     // PipeMonitor may still fail to update DB later.
   2040                     throw new FileNotFoundException(uri.toString());
   2041                 }
   2042             }
   2043 
   2044             try {
   2045                 ParcelFileDescriptor[] pipeFds = ParcelFileDescriptor.createPipe();
   2046                 PipeMonitor pipeMonitor = new PipeMonitor(pipeFds[0], channelId, params);
   2047                 pipeMonitor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
   2048                 return pipeFds[1];
   2049             } catch (IOException ioe) {
   2050                 FileNotFoundException fne = new FileNotFoundException(uri.toString());
   2051                 fne.initCause(ioe);
   2052                 throw fne;
   2053             }
   2054         }
   2055     }
   2056 
   2057     /**
   2058      * Validates the sort order based on the given field set.
   2059      *
   2060      * @throws IllegalArgumentException if there is any unknown field.
   2061      */
   2062     @SuppressLint("DefaultLocale")
   2063     private static void validateSortOrder(String sortOrder, Set<String> possibleFields) {
   2064         if (TextUtils.isEmpty(sortOrder) || possibleFields.isEmpty()) {
   2065             return;
   2066         }
   2067         String[] orders = sortOrder.split(",");
   2068         for (String order : orders) {
   2069             String field = order.replaceAll("\\s+", " ").trim().toLowerCase().replace(" asc", "")
   2070                     .replace(" desc", "");
   2071             if (!possibleFields.contains(field)) {
   2072                 throw new IllegalArgumentException("Illegal field in sort order " + order);
   2073             }
   2074         }
   2075     }
   2076 
   2077     private class PipeMonitor extends AsyncTask<Void, Void, Void> {
   2078         private final ParcelFileDescriptor mPfd;
   2079         private final long mChannelId;
   2080         private final SqlParams mParams;
   2081 
   2082         private PipeMonitor(ParcelFileDescriptor pfd, long channelId, SqlParams params) {
   2083             mPfd = pfd;
   2084             mChannelId = channelId;
   2085             mParams = params;
   2086         }
   2087 
   2088         @Override
   2089         protected Void doInBackground(Void... params) {
   2090             AutoCloseInputStream is = new AutoCloseInputStream(mPfd);
   2091             ByteArrayOutputStream baos = null;
   2092             int count = 0;
   2093             try {
   2094                 Bitmap bitmap = BitmapFactory.decodeStream(is);
   2095                 if (bitmap == null) {
   2096                     Log.e(TAG, "Failed to decode logo image for channel ID " + mChannelId);
   2097                     return null;
   2098                 }
   2099 
   2100                 float scaleFactor = Math.min(1f, ((float) MAX_LOGO_IMAGE_SIZE) /
   2101                         Math.max(bitmap.getWidth(), bitmap.getHeight()));
   2102                 if (scaleFactor < 1f) {
   2103                     bitmap = Bitmap.createScaledBitmap(bitmap,
   2104                             (int) (bitmap.getWidth() * scaleFactor),
   2105                             (int) (bitmap.getHeight() * scaleFactor), false);
   2106                 }
   2107 
   2108                 baos = new ByteArrayOutputStream();
   2109                 bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
   2110                 byte[] bytes = baos.toByteArray();
   2111 
   2112                 ContentValues values = new ContentValues();
   2113                 values.put(CHANNELS_COLUMN_LOGO, bytes);
   2114 
   2115                 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   2116                 count = db.update(mParams.getTables(), values, mParams.getSelection(),
   2117                         mParams.getSelectionArgs());
   2118                 if (count > 0) {
   2119                     Uri uri = TvContract.buildChannelLogoUri(mChannelId);
   2120                     notifyChange(uri);
   2121                 }
   2122             } finally {
   2123                 if (count == 0) {
   2124                     try {
   2125                         mPfd.closeWithError("Failed to write logo for channel ID " + mChannelId);
   2126                     } catch (IOException ioe) {
   2127                         Log.e(TAG, "Failed to close pipe", ioe);
   2128                     }
   2129                 }
   2130                 IoUtils.closeQuietly(baos);
   2131                 IoUtils.closeQuietly(is);
   2132             }
   2133             return null;
   2134         }
   2135     }
   2136 
   2137     private void deleteUnconsolidatedWatchedProgramsRows() {
   2138         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   2139         db.delete(WATCHED_PROGRAMS_TABLE, WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0", null);
   2140     }
   2141 
   2142     @SuppressLint("HandlerLeak")
   2143     private final class WatchLogHandler extends Handler {
   2144         private static final int MSG_CONSOLIDATE = 1;
   2145         private static final int MSG_TRY_CONSOLIDATE_ALL = 2;
   2146 
   2147         @Override
   2148         public void handleMessage(Message msg) {
   2149             switch (msg.what) {
   2150                 case MSG_CONSOLIDATE: {
   2151                     SomeArgs args = (SomeArgs) msg.obj;
   2152                     String sessionToken = (String) args.arg1;
   2153                     long watchEndTime = (long) args.arg2;
   2154                     onConsolidate(sessionToken, watchEndTime);
   2155                     args.recycle();
   2156                     return;
   2157                 }
   2158                 case MSG_TRY_CONSOLIDATE_ALL: {
   2159                     onTryConsolidateAll();
   2160                     return;
   2161                 }
   2162                 default: {
   2163                     Log.w(TAG, "Unhandled message code: " + msg.what);
   2164                     return;
   2165                 }
   2166             }
   2167         }
   2168 
   2169         // Consolidates all WatchedPrograms rows for a given session with watch end time information
   2170         // of the most recent log entry. After this method is called, it is guaranteed that there
   2171         // remain consolidated rows only for that session.
   2172         private void onConsolidate(String sessionToken, long watchEndTime) {
   2173             if (DEBUG) {
   2174                 Log.d(TAG, "onConsolidate(sessionToken=" + sessionToken + ", watchEndTime="
   2175                         + watchEndTime + ")");
   2176             }
   2177 
   2178             SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
   2179             queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
   2180             SQLiteDatabase db = mOpenHelper.getReadableDatabase();
   2181 
   2182             // Pick up the last row with the same session token.
   2183             String[] projection = {
   2184                     WatchedPrograms._ID,
   2185                     WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
   2186                     WatchedPrograms.COLUMN_CHANNEL_ID
   2187             };
   2188             String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=? AND "
   2189                     + WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + "=?";
   2190             String[] selectionArgs = {
   2191                     "0",
   2192                     sessionToken
   2193             };
   2194             String sortOrder = WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC";
   2195 
   2196             int consolidatedRowCount = 0;
   2197             try (Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null,
   2198                     null, sortOrder)) {
   2199                 long oldWatchStartTime = watchEndTime;
   2200                 while (cursor != null && cursor.moveToNext()) {
   2201                     long id = cursor.getLong(0);
   2202                     long watchStartTime = cursor.getLong(1);
   2203                     long channelId = cursor.getLong(2);
   2204                     consolidatedRowCount += consolidateRow(id, watchStartTime, oldWatchStartTime,
   2205                             channelId, false);
   2206                     oldWatchStartTime = watchStartTime;
   2207                 }
   2208             }
   2209             if (consolidatedRowCount > 0) {
   2210                 deleteUnsearchable();
   2211             }
   2212         }
   2213 
   2214         // Tries to consolidate all WatchedPrograms rows regardless of the session. After this
   2215         // method is called, it is guaranteed that we have at most one unconsolidated log entry per
   2216         // session that represents the user's ongoing watch activity.
   2217         // Also, this method automatically schedules the next consolidation if there still remains
   2218         // an unconsolidated entry.
   2219         private void onTryConsolidateAll() {
   2220             if (DEBUG) {
   2221                 Log.d(TAG, "onTryConsolidateAll()");
   2222             }
   2223 
   2224             SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
   2225             queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
   2226             SQLiteDatabase db = mOpenHelper.getReadableDatabase();
   2227 
   2228             // Pick up all unconsolidated rows grouped by session. The most recent log entry goes on
   2229             // top.
   2230             String[] projection = {
   2231                     WatchedPrograms._ID,
   2232                     WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
   2233                     WatchedPrograms.COLUMN_CHANNEL_ID,
   2234                     WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN
   2235             };
   2236             String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0";
   2237             String sortOrder = WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN + " DESC,"
   2238                     + WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS + " DESC";
   2239 
   2240             int consolidatedRowCount = 0;
   2241             try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null,
   2242                     sortOrder)) {
   2243                 long oldWatchStartTime = 0;
   2244                 String oldSessionToken = null;
   2245                 while (cursor != null && cursor.moveToNext()) {
   2246                     long id = cursor.getLong(0);
   2247                     long watchStartTime = cursor.getLong(1);
   2248                     long channelId = cursor.getLong(2);
   2249                     String sessionToken = cursor.getString(3);
   2250 
   2251                     if (!sessionToken.equals(oldSessionToken)) {
   2252                         // The most recent log entry for the current session, which may be still
   2253                         // active. Just go through a dry run with the current time to see if this
   2254                         // entry can be split into multiple rows.
   2255                         consolidatedRowCount += consolidateRow(id, watchStartTime,
   2256                                 System.currentTimeMillis(), channelId, true);
   2257                         oldSessionToken = sessionToken;
   2258                     } else {
   2259                         // The later entries after the most recent one all fall into here. We now
   2260                         // know that this watch activity ended exactly at the same time when the
   2261                         // next activity started.
   2262                         consolidatedRowCount += consolidateRow(id, watchStartTime,
   2263                                 oldWatchStartTime, channelId, false);
   2264                     }
   2265                     oldWatchStartTime = watchStartTime;
   2266                 }
   2267             }
   2268             if (consolidatedRowCount > 0) {
   2269                 deleteUnsearchable();
   2270             }
   2271             scheduleConsolidationIfNeeded();
   2272         }
   2273 
   2274         // Consolidates a WatchedPrograms row.
   2275         // A row is 'consolidated' if and only if the following information is complete:
   2276         // 1. WatchedPrograms.COLUMN_CHANNEL_ID
   2277         // 2. WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS
   2278         // 3. WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS
   2279         // where COLUMN_WATCH_START_TIME_UTC_MILLIS <= COLUMN_WATCH_END_TIME_UTC_MILLIS.
   2280         // This is the minimal but useful enough set of information to comprise the user's watch
   2281         // history. (The program data are considered optional although we do try to fill them while
   2282         // consolidating the row.) It is guaranteed that the target row is either consolidated or
   2283         // deleted after this method is called.
   2284         // Set {@code dryRun} to {@code true} if you think it's necessary to split the row without
   2285         // consolidating the most recent row because the user stayed on the same channel for a very
   2286         // long time.
   2287         // This method returns the number of consolidated rows, which can be 0 or more.
   2288         private int consolidateRow(
   2289                 long id, long watchStartTime, long watchEndTime, long channelId, boolean dryRun) {
   2290             if (DEBUG) {
   2291                 Log.d(TAG, "consolidateRow(id=" + id + ", watchStartTime=" + watchStartTime
   2292                         + ", watchEndTime=" + watchEndTime + ", channelId=" + channelId
   2293                         + ", dryRun=" + dryRun + ")");
   2294             }
   2295 
   2296             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   2297 
   2298             if (watchStartTime > watchEndTime) {
   2299                 Log.e(TAG, "watchEndTime cannot be less than watchStartTime");
   2300                 db.delete(WATCHED_PROGRAMS_TABLE, WatchedPrograms._ID + "=" + String.valueOf(id),
   2301                         null);
   2302                 return 0;
   2303             }
   2304 
   2305             ContentValues values = getProgramValues(channelId, watchStartTime);
   2306             Long endTime = values.getAsLong(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS);
   2307             boolean needsToSplit = endTime != null && endTime < watchEndTime;
   2308 
   2309             values.put(WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
   2310                     String.valueOf(watchStartTime));
   2311             if (!dryRun || needsToSplit) {
   2312                 values.put(WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS,
   2313                         String.valueOf(needsToSplit ? endTime : watchEndTime));
   2314                 values.put(WATCHED_PROGRAMS_COLUMN_CONSOLIDATED, "1");
   2315                 db.update(WATCHED_PROGRAMS_TABLE, values,
   2316                         WatchedPrograms._ID + "=" + String.valueOf(id), null);
   2317                 // Treat the watched program is inserted when WATCHED_PROGRAMS_COLUMN_CONSOLIDATED
   2318                 // becomes 1.
   2319                 notifyChange(TvContract.buildWatchedProgramUri(id));
   2320             } else {
   2321                 db.update(WATCHED_PROGRAMS_TABLE, values,
   2322                         WatchedPrograms._ID + "=" + String.valueOf(id), null);
   2323             }
   2324             int count = dryRun ? 0 : 1;
   2325             if (needsToSplit) {
   2326                 // This means that the program ended before the user stops watching the current
   2327                 // channel. In this case we duplicate the log entry as many as the number of
   2328                 // programs watched on the same channel. Here the end time of the current program
   2329                 // becomes the new watch start time of the next program.
   2330                 long duplicatedId = duplicateRow(id);
   2331                 if (duplicatedId > 0) {
   2332                     count += consolidateRow(duplicatedId, endTime, watchEndTime, channelId, dryRun);
   2333                 }
   2334             }
   2335             return count;
   2336         }
   2337 
   2338         // Deletes the log entries from unsearchable channels. Note that only consolidated log
   2339         // entries are safe to delete.
   2340         private void deleteUnsearchable() {
   2341             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   2342             String deleteWhere = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=1 AND "
   2343                     + WatchedPrograms.COLUMN_CHANNEL_ID + " IN (SELECT " + Channels._ID
   2344                     + " FROM " + CHANNELS_TABLE + " WHERE " + Channels.COLUMN_SEARCHABLE + "=0)";
   2345             db.delete(WATCHED_PROGRAMS_TABLE, deleteWhere, null);
   2346         }
   2347 
   2348         private void scheduleConsolidationIfNeeded() {
   2349             if (DEBUG) {
   2350                 Log.d(TAG, "scheduleConsolidationIfNeeded()");
   2351             }
   2352             SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
   2353             queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
   2354             SQLiteDatabase db = mOpenHelper.getReadableDatabase();
   2355 
   2356             // Pick up all unconsolidated rows.
   2357             String[] projection = {
   2358                     WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS,
   2359                     WatchedPrograms.COLUMN_CHANNEL_ID,
   2360             };
   2361             String selection = WATCHED_PROGRAMS_COLUMN_CONSOLIDATED + "=0";
   2362 
   2363             try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null,
   2364                     null)) {
   2365                 // Find the earliest time that any of the currently watching programs ends and
   2366                 // schedule the next consolidation at that time.
   2367                 long minEndTime = Long.MAX_VALUE;
   2368                 while (cursor != null && cursor.moveToNext()) {
   2369                     long watchStartTime = cursor.getLong(0);
   2370                     long channelId = cursor.getLong(1);
   2371                     ContentValues values = getProgramValues(channelId, watchStartTime);
   2372                     Long endTime = values.getAsLong(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS);
   2373 
   2374                     if (endTime != null && endTime < minEndTime
   2375                             && endTime > System.currentTimeMillis()) {
   2376                         minEndTime = endTime;
   2377                     }
   2378                 }
   2379                 if (minEndTime != Long.MAX_VALUE) {
   2380                     sendEmptyMessageAtTime(MSG_TRY_CONSOLIDATE_ALL, minEndTime);
   2381                     if (DEBUG) {
   2382                         CharSequence minEndTimeStr = DateUtils.getRelativeTimeSpanString(
   2383                                 minEndTime, System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS);
   2384                         Log.d(TAG, "onTryConsolidateAll() scheduled " + minEndTimeStr);
   2385                     }
   2386                 }
   2387             }
   2388         }
   2389 
   2390         // Returns non-null ContentValues of the program data that the user watched on the channel
   2391         // {@code channelId} at the time {@code time}.
   2392         private ContentValues getProgramValues(long channelId, long time) {
   2393             SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
   2394             queryBuilder.setTables(PROGRAMS_TABLE);
   2395             SQLiteDatabase db = mOpenHelper.getReadableDatabase();
   2396 
   2397             String[] projection = {
   2398                     Programs.COLUMN_TITLE,
   2399                     Programs.COLUMN_START_TIME_UTC_MILLIS,
   2400                     Programs.COLUMN_END_TIME_UTC_MILLIS,
   2401                     Programs.COLUMN_SHORT_DESCRIPTION
   2402             };
   2403             String selection = Programs.COLUMN_CHANNEL_ID + "=? AND "
   2404                     + Programs.COLUMN_START_TIME_UTC_MILLIS + "<=? AND "
   2405                     + Programs.COLUMN_END_TIME_UTC_MILLIS + ">?";
   2406             String[] selectionArgs = {
   2407                     String.valueOf(channelId),
   2408                     String.valueOf(time),
   2409                     String.valueOf(time)
   2410             };
   2411             String sortOrder = Programs.COLUMN_START_TIME_UTC_MILLIS + " ASC";
   2412 
   2413             try (Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null,
   2414                     null, sortOrder)) {
   2415                 ContentValues values = new ContentValues();
   2416                 if (cursor != null && cursor.moveToNext()) {
   2417                     values.put(WatchedPrograms.COLUMN_TITLE, cursor.getString(0));
   2418                     values.put(WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS, cursor.getLong(1));
   2419                     values.put(WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS, cursor.getLong(2));
   2420                     values.put(WatchedPrograms.COLUMN_DESCRIPTION, cursor.getString(3));
   2421                 }
   2422                 return values;
   2423             }
   2424         }
   2425 
   2426         // Duplicates the WatchedPrograms row with a given ID and returns the ID of the duplicated
   2427         // row. Returns -1 if failed.
   2428         private long duplicateRow(long id) {
   2429             if (DEBUG) {
   2430                 Log.d(TAG, "duplicateRow(" + id + ")");
   2431             }
   2432 
   2433             SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
   2434             queryBuilder.setTables(WATCHED_PROGRAMS_TABLE);
   2435             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   2436 
   2437             String[] projection = {
   2438                     WatchedPrograms.COLUMN_PACKAGE_NAME,
   2439                     WatchedPrograms.COLUMN_CHANNEL_ID,
   2440                     WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN
   2441             };
   2442             String selection = WatchedPrograms._ID + "=" + String.valueOf(id);
   2443 
   2444             try (Cursor cursor = queryBuilder.query(db, projection, selection, null, null, null,
   2445                     null)) {
   2446                 long rowId = -1;
   2447                 if (cursor != null && cursor.moveToNext()) {
   2448                     ContentValues values = new ContentValues();
   2449                     values.put(WatchedPrograms.COLUMN_PACKAGE_NAME, cursor.getString(0));
   2450                     values.put(WatchedPrograms.COLUMN_CHANNEL_ID, cursor.getLong(1));
   2451                     values.put(WatchedPrograms.COLUMN_INTERNAL_SESSION_TOKEN, cursor.getString(2));
   2452                     rowId = db.insert(WATCHED_PROGRAMS_TABLE, null, values);
   2453                 }
   2454                 return rowId;
   2455             }
   2456         }
   2457     }
   2458 }
   2459