Home | History | Annotate | Download | only in search
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tv.search;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.database.Cursor;
     23 import android.media.tv.TvContentRating;
     24 import android.media.tv.TvContract;
     25 import android.media.tv.TvContract.Channels;
     26 import android.media.tv.TvContract.Programs;
     27 import android.media.tv.TvContract.WatchedPrograms;
     28 import android.media.tv.TvInputInfo;
     29 import android.media.tv.TvInputManager;
     30 import android.net.Uri;
     31 import android.os.SystemClock;
     32 import android.support.annotation.WorkerThread;
     33 import android.text.TextUtils;
     34 import android.util.Log;
     35 import com.android.tv.common.TvContentRatingCache;
     36 import com.android.tv.common.util.PermissionUtils;
     37 import com.android.tv.search.LocalSearchProvider.SearchResult;
     38 import com.android.tv.util.Utils;
     39 import java.util.ArrayList;
     40 import java.util.Collections;
     41 import java.util.Comparator;
     42 import java.util.HashMap;
     43 import java.util.HashSet;
     44 import java.util.List;
     45 import java.util.Locale;
     46 import java.util.Map;
     47 import java.util.Objects;
     48 import java.util.Set;
     49 import java.util.concurrent.TimeUnit;
     50 
     51 /** An implementation of {@link SearchInterface} to search query from TvProvider directly. */
     52 public class TvProviderSearch implements SearchInterface {
     53     private static final String TAG = "TvProviderSearch";
     54     private static final boolean DEBUG = false;
     55 
     56     private static final long SEARCH_TIME_FRAME_MS = TimeUnit.DAYS.toMillis(14);
     57 
     58     private static final int NO_LIMIT = 0;
     59 
     60     private final Context mContext;
     61     private final ContentResolver mContentResolver;
     62     private final TvInputManager mTvInputManager;
     63     private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance();
     64 
     65     TvProviderSearch(Context context) {
     66         mContext = context;
     67         mContentResolver = context.getContentResolver();
     68         mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
     69     }
     70 
     71     /**
     72      * Search channels, inputs, or programs from TvProvider. This assumes that parental control
     73      * settings will not be change while searching.
     74      *
     75      * @param action One of {@link #ACTION_TYPE_SWITCH_CHANNEL}, {@link #ACTION_TYPE_SWITCH_INPUT},
     76      *     or {@link #ACTION_TYPE_AMBIGUOUS},
     77      */
     78     @Override
     79     @WorkerThread
     80     public List<SearchResult> search(String query, int limit, int action) {
     81         // TODO(b/72499463): add a test.
     82         List<SearchResult> results = new ArrayList<>();
     83         if (!PermissionUtils.hasAccessAllEpg(mContext)) {
     84             // TODO: support this feature for non-system LC app. b/23939816
     85             return results;
     86         }
     87         Set<Long> channelsFound = new HashSet<>();
     88         if (action == ACTION_TYPE_SWITCH_CHANNEL) {
     89             results.addAll(searchChannels(query, channelsFound, limit));
     90         } else if (action == ACTION_TYPE_SWITCH_INPUT) {
     91             results.addAll(searchInputs(query, limit));
     92         } else {
     93             // Search channels first.
     94             results.addAll(searchChannels(query, channelsFound, limit));
     95             if (results.size() >= limit) {
     96                 return results;
     97             }
     98 
     99             // In case the user wanted to perform the action "switch to XXX", which is indicated by
    100             // setting the limit to 1, search inputs.
    101             if (limit == 1) {
    102                 results.addAll(searchInputs(query, limit));
    103                 if (!results.isEmpty()) {
    104                     return results;
    105                 }
    106             }
    107 
    108             // Lastly, search programs.
    109             limit -= results.size();
    110             results.addAll(
    111                     searchPrograms(
    112                             query,
    113                             null,
    114                             new String[] {Programs.COLUMN_TITLE, Programs.COLUMN_SHORT_DESCRIPTION},
    115                             channelsFound,
    116                             limit));
    117         }
    118         return results;
    119     }
    120 
    121     private void appendSelectionString(
    122             StringBuilder sb, String[] columnForExactMatching, String[] columnForPartialMatching) {
    123         boolean firstColumn = true;
    124         if (columnForExactMatching != null) {
    125             for (String column : columnForExactMatching) {
    126                 if (!firstColumn) {
    127                     sb.append(" OR ");
    128                 } else {
    129                     firstColumn = false;
    130                 }
    131                 sb.append(column).append("=?");
    132             }
    133         }
    134         if (columnForPartialMatching != null) {
    135             for (String column : columnForPartialMatching) {
    136                 if (!firstColumn) {
    137                     sb.append(" OR ");
    138                 } else {
    139                     firstColumn = false;
    140                 }
    141                 sb.append(column).append(" LIKE ?");
    142             }
    143         }
    144     }
    145 
    146     private void insertSelectionArgumentStrings(
    147             String[] selectionArgs,
    148             int pos,
    149             String query,
    150             String[] columnForExactMatching,
    151             String[] columnForPartialMatching) {
    152         if (columnForExactMatching != null) {
    153             int until = pos + columnForExactMatching.length;
    154             for (; pos < until; ++pos) {
    155                 selectionArgs[pos] = query;
    156             }
    157         }
    158         String selectionArg = "%" + query + "%";
    159         if (columnForPartialMatching != null) {
    160             int until = pos + columnForPartialMatching.length;
    161             for (; pos < until; ++pos) {
    162                 selectionArgs[pos] = selectionArg;
    163             }
    164         }
    165     }
    166 
    167     @WorkerThread
    168     private List<SearchResult> searchChannels(String query, Set<Long> channels, int limit) {
    169         if (DEBUG) Log.d(TAG, "Searching channels: '" + query + "'");
    170         long time = SystemClock.elapsedRealtime();
    171         List<SearchResult> results = new ArrayList<>();
    172         if (TextUtils.isDigitsOnly(query)) {
    173             results.addAll(
    174                     searchChannels(
    175                             query,
    176                             new String[] {Channels.COLUMN_DISPLAY_NUMBER},
    177                             null,
    178                             channels,
    179                             NO_LIMIT));
    180             if (results.size() > 1) {
    181                 Collections.sort(results, new ChannelComparatorWithSameDisplayNumber());
    182             }
    183         }
    184         if (results.size() < limit) {
    185             results.addAll(
    186                     searchChannels(
    187                             query,
    188                             null,
    189                             new String[] {
    190                                 Channels.COLUMN_DISPLAY_NAME, Channels.COLUMN_DESCRIPTION
    191                             },
    192                             channels,
    193                             limit - results.size()));
    194         }
    195         if (results.size() > limit) {
    196             results = results.subList(0, limit);
    197         }
    198         for (int i = 0; i < results.size(); i++) {
    199             results.set(i, fillProgramInfo(results.get(i)));
    200         }
    201         if (DEBUG) {
    202             Log.d(
    203                     TAG,
    204                     "Found "
    205                             + results.size()
    206                             + " channels. Elapsed time for searching"
    207                             + " channels: "
    208                             + (SystemClock.elapsedRealtime() - time)
    209                             + "(msec)");
    210         }
    211         return results;
    212     }
    213 
    214     @WorkerThread
    215     private List<SearchResult> searchChannels(
    216             String query,
    217             String[] columnForExactMatching,
    218             String[] columnForPartialMatching,
    219             Set<Long> channelsFound,
    220             int limit) {
    221         String[] projection = {
    222             Channels._ID,
    223             Channels.COLUMN_DISPLAY_NUMBER,
    224             Channels.COLUMN_DISPLAY_NAME,
    225             Channels.COLUMN_DESCRIPTION
    226         };
    227 
    228         StringBuilder sb = new StringBuilder();
    229         sb.append(Channels.COLUMN_BROWSABLE)
    230                 .append("=1 AND ")
    231                 .append(Channels.COLUMN_SEARCHABLE)
    232                 .append("=1");
    233         if (mTvInputManager.isParentalControlsEnabled()) {
    234             sb.append(" AND ").append(Channels.COLUMN_LOCKED).append("=0");
    235         }
    236         sb.append(" AND (");
    237         appendSelectionString(sb, columnForExactMatching, columnForPartialMatching);
    238         sb.append(")");
    239         String selection = sb.toString();
    240 
    241         int len =
    242                 (columnForExactMatching == null ? 0 : columnForExactMatching.length)
    243                         + (columnForPartialMatching == null ? 0 : columnForPartialMatching.length);
    244         String[] selectionArgs = new String[len];
    245         insertSelectionArgumentStrings(
    246                 selectionArgs, 0, query, columnForExactMatching, columnForPartialMatching);
    247 
    248         List<SearchResult> searchResults = new ArrayList<>();
    249 
    250         try (Cursor c =
    251                 mContentResolver.query(
    252                         Channels.CONTENT_URI, projection, selection, selectionArgs, null)) {
    253             if (c != null) {
    254                 int count = 0;
    255                 while (c.moveToNext()) {
    256                     long id = c.getLong(0);
    257                     // Filter out the channel which has been already searched.
    258                     if (channelsFound.contains(id)) {
    259                         continue;
    260                     }
    261                     channelsFound.add(id);
    262 
    263                     SearchResult.Builder result = SearchResult.builder();
    264                     result.setChannelId(id);
    265                     result.setChannelNumber(c.getString(1));
    266                     result.setTitle(c.getString(2));
    267                     result.setDescription(c.getString(3));
    268                     result.setImageUri(TvContract.buildChannelLogoUri(id).toString());
    269                     result.setIntentAction(Intent.ACTION_VIEW);
    270                     result.setIntentData(buildIntentData(id));
    271                     result.setContentType(Programs.CONTENT_ITEM_TYPE);
    272                     result.setIsLive(true);
    273                     result.setProgressPercentage(LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE);
    274 
    275                     searchResults.add(result.build());
    276 
    277                     if (limit != NO_LIMIT && ++count >= limit) {
    278                         break;
    279                     }
    280                 }
    281             }
    282         }
    283         return searchResults;
    284     }
    285 
    286     /**
    287      * Replaces the channel information - title, description, channel logo - with the current
    288      * program information of the channel if the current program information exists and it is not
    289      * blocked.
    290      */
    291     @WorkerThread
    292     private SearchResult fillProgramInfo(SearchResult result) {
    293         long now = System.currentTimeMillis();
    294         Uri uri = TvContract.buildProgramsUriForChannel(result.getChannelId(), now, now);
    295         String[] projection =
    296                 new String[] {
    297                     Programs.COLUMN_TITLE,
    298                     Programs.COLUMN_POSTER_ART_URI,
    299                     Programs.COLUMN_CONTENT_RATING,
    300                     Programs.COLUMN_VIDEO_WIDTH,
    301                     Programs.COLUMN_VIDEO_HEIGHT,
    302                     Programs.COLUMN_START_TIME_UTC_MILLIS,
    303                     Programs.COLUMN_END_TIME_UTC_MILLIS
    304                 };
    305 
    306         try (Cursor c = mContentResolver.query(uri, projection, null, null, null)) {
    307             if (c != null && c.moveToNext() && !isRatingBlocked(c.getString(2))) {
    308                 String channelName = result.getTitle();
    309                 String channelNumber = result.getChannelNumber();
    310                 SearchResult.Builder builder = SearchResult.builder();
    311                 long startUtcMillis = c.getLong(5);
    312                 long endUtcMillis = c.getLong(6);
    313                 builder.setTitle(c.getString(0));
    314                 builder.setDescription(
    315                         buildProgramDescription(
    316                                 channelNumber, channelName, startUtcMillis, endUtcMillis));
    317                 String imageUri = c.getString(1);
    318                 if (imageUri != null) {
    319                     builder.setImageUri(imageUri);
    320                 }
    321                 builder.setVideoWidth(c.getInt(3));
    322                 builder.setVideoHeight(c.getInt(4));
    323                 builder.setDuration(endUtcMillis - startUtcMillis);
    324                 builder.setProgressPercentage(getProgressPercentage(startUtcMillis, endUtcMillis));
    325                 return builder.build();
    326             }
    327         }
    328         return result;
    329     }
    330 
    331     private String buildProgramDescription(
    332             String channelNumber,
    333             String channelName,
    334             long programStartUtcMillis,
    335             long programEndUtcMillis) {
    336         return Utils.getDurationString(mContext, programStartUtcMillis, programEndUtcMillis, false)
    337                 + System.lineSeparator()
    338                 + channelNumber
    339                 + " "
    340                 + channelName;
    341     }
    342 
    343     private int getProgressPercentage(long startUtcMillis, long endUtcMillis) {
    344         long current = System.currentTimeMillis();
    345         if (startUtcMillis > current || endUtcMillis <= current) {
    346             return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE;
    347         }
    348         return (int) (100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis));
    349     }
    350 
    351     @WorkerThread
    352     private List<SearchResult> searchPrograms(
    353             String query,
    354             String[] columnForExactMatching,
    355             String[] columnForPartialMatching,
    356             Set<Long> channelsFound,
    357             int limit) {
    358         if (DEBUG) Log.d(TAG, "Searching programs: '" + query + "'");
    359         long time = SystemClock.elapsedRealtime();
    360         String[] projection = {
    361             Programs.COLUMN_CHANNEL_ID,
    362             Programs.COLUMN_TITLE,
    363             Programs.COLUMN_POSTER_ART_URI,
    364             Programs.COLUMN_CONTENT_RATING,
    365             Programs.COLUMN_VIDEO_WIDTH,
    366             Programs.COLUMN_VIDEO_HEIGHT,
    367             Programs.COLUMN_START_TIME_UTC_MILLIS,
    368             Programs.COLUMN_END_TIME_UTC_MILLIS,
    369             Programs._ID
    370         };
    371 
    372         StringBuilder sb = new StringBuilder();
    373         // Search among the programs which are now being on the air.
    374         sb.append(Programs.COLUMN_START_TIME_UTC_MILLIS).append("<=? AND ");
    375         sb.append(Programs.COLUMN_END_TIME_UTC_MILLIS).append(">=? AND (");
    376         appendSelectionString(sb, columnForExactMatching, columnForPartialMatching);
    377         sb.append(")");
    378         String selection = sb.toString();
    379 
    380         int len =
    381                 (columnForExactMatching == null ? 0 : columnForExactMatching.length)
    382                         + (columnForPartialMatching == null ? 0 : columnForPartialMatching.length);
    383         String[] selectionArgs = new String[len + 2];
    384         long now = System.currentTimeMillis();
    385         selectionArgs[0] = String.valueOf(now + SEARCH_TIME_FRAME_MS);
    386         selectionArgs[1] = String.valueOf(now);
    387         insertSelectionArgumentStrings(
    388                 selectionArgs, 2, query, columnForExactMatching, columnForPartialMatching);
    389 
    390         List<SearchResult> searchResults = new ArrayList<>();
    391 
    392         try (Cursor c =
    393                 mContentResolver.query(
    394                         Programs.CONTENT_URI, projection, selection, selectionArgs, null)) {
    395             if (c != null) {
    396                 int count = 0;
    397                 while (c.moveToNext()) {
    398                     long id = c.getLong(0);
    399                     // Filter out the program whose channel is already searched.
    400                     if (channelsFound.contains(id)) {
    401                         continue;
    402                     }
    403                     channelsFound.add(id);
    404 
    405                     // Don't know whether the channel is searchable or not.
    406                     String[] channelProjection = {
    407                         Channels._ID, Channels.COLUMN_DISPLAY_NUMBER, Channels.COLUMN_DISPLAY_NAME
    408                     };
    409                     sb = new StringBuilder();
    410                     sb.append(Channels._ID)
    411                             .append("=? AND ")
    412                             .append(Channels.COLUMN_BROWSABLE)
    413                             .append("=1 AND ")
    414                             .append(Channels.COLUMN_SEARCHABLE)
    415                             .append("=1");
    416                     if (mTvInputManager.isParentalControlsEnabled()) {
    417                         sb.append(" AND ").append(Channels.COLUMN_LOCKED).append("=0");
    418                     }
    419                     String selectionChannel = sb.toString();
    420                     try (Cursor cChannel =
    421                             mContentResolver.query(
    422                                     Channels.CONTENT_URI,
    423                                     channelProjection,
    424                                     selectionChannel,
    425                                     new String[] {String.valueOf(id)},
    426                                     null)) {
    427                         if (cChannel != null
    428                                 && cChannel.moveToNext()
    429                                 && !isRatingBlocked(c.getString(3))) {
    430                             long startUtcMillis = c.getLong(6);
    431                             long endUtcMillis = c.getLong(7);
    432                             SearchResult.Builder result = SearchResult.builder();
    433                             result.setChannelId(c.getLong(0));
    434                             result.setTitle(c.getString(1));
    435                             result.setDescription(
    436                                     buildProgramDescription(
    437                                             cChannel.getString(1),
    438                                             cChannel.getString(2),
    439                                             startUtcMillis,
    440                                             endUtcMillis));
    441                             result.setImageUri(c.getString(2));
    442                             result.setIntentAction(Intent.ACTION_VIEW);
    443                             result.setIntentData(buildIntentData(id));
    444                             result.setIntentExtraData(
    445                                     TvContract.buildProgramUri(c.getLong(8)).toString());
    446                             result.setContentType(Programs.CONTENT_ITEM_TYPE);
    447                             result.setIsLive(true);
    448                             result.setVideoWidth(c.getInt(4));
    449                             result.setVideoHeight(c.getInt(5));
    450                             result.setDuration(endUtcMillis - startUtcMillis);
    451                             result.setProgressPercentage(
    452                                     getProgressPercentage(startUtcMillis, endUtcMillis));
    453                             searchResults.add(result.build());
    454 
    455                             if (limit != NO_LIMIT && ++count >= limit) {
    456                                 break;
    457                             }
    458                         }
    459                     }
    460                 }
    461             }
    462         }
    463         if (DEBUG) {
    464             Log.d(
    465                     TAG,
    466                     "Found "
    467                             + searchResults.size()
    468                             + " programs. Elapsed time for searching"
    469                             + " programs: "
    470                             + (SystemClock.elapsedRealtime() - time)
    471                             + "(msec)");
    472         }
    473         return searchResults;
    474     }
    475 
    476     private String buildIntentData(long channelId) {
    477         return TvContract.buildChannelUri(channelId).toString();
    478     }
    479 
    480     private boolean isRatingBlocked(String ratings) {
    481         if (TextUtils.isEmpty(ratings) || !mTvInputManager.isParentalControlsEnabled()) {
    482             return false;
    483         }
    484         TvContentRating[] ratingArray = mTvContentRatingCache.getRatings(ratings);
    485         if (ratingArray != null) {
    486             for (TvContentRating r : ratingArray) {
    487                 if (mTvInputManager.isRatingBlocked(r)) {
    488                     return true;
    489                 }
    490             }
    491         }
    492         return false;
    493     }
    494 
    495     private List<SearchResult> searchInputs(String query, int limit) {
    496         if (DEBUG) Log.d(TAG, "Searching inputs: '" + query + "'");
    497         long time = SystemClock.elapsedRealtime();
    498 
    499         query = canonicalizeLabel(query);
    500         List<TvInputInfo> inputList = mTvInputManager.getTvInputList();
    501         List<SearchResult> results = new ArrayList<>();
    502 
    503         // Find exact matches first.
    504         for (TvInputInfo input : inputList) {
    505             if (input.getType() == TvInputInfo.TYPE_TUNER) {
    506                 continue;
    507             }
    508             String label = canonicalizeLabel(input.loadLabel(mContext));
    509             String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext));
    510             if (TextUtils.equals(query, label) || TextUtils.equals(query, customLabel)) {
    511                 results.add(buildSearchResultForInput(input.getId()));
    512                 if (results.size() >= limit) {
    513                     if (DEBUG) {
    514                         Log.d(
    515                                 TAG,
    516                                 "Found "
    517                                         + results.size()
    518                                         + " inputs. Elapsed time for"
    519                                         + " searching inputs: "
    520                                         + (SystemClock.elapsedRealtime() - time)
    521                                         + "(msec)");
    522                     }
    523                     return results;
    524                 }
    525             }
    526         }
    527 
    528         // Then look for partial matches.
    529         for (TvInputInfo input : inputList) {
    530             if (input.getType() == TvInputInfo.TYPE_TUNER) {
    531                 continue;
    532             }
    533             String label = canonicalizeLabel(input.loadLabel(mContext));
    534             String customLabel = canonicalizeLabel(input.loadCustomLabel(mContext));
    535             if ((label != null && label.contains(query))
    536                     || (customLabel != null && customLabel.contains(query))) {
    537                 results.add(buildSearchResultForInput(input.getId()));
    538                 if (results.size() >= limit) {
    539                     if (DEBUG) {
    540                         Log.d(
    541                                 TAG,
    542                                 "Found "
    543                                         + results.size()
    544                                         + " inputs. Elapsed time for"
    545                                         + " searching inputs: "
    546                                         + (SystemClock.elapsedRealtime() - time)
    547                                         + "(msec)");
    548                     }
    549                     return results;
    550                 }
    551             }
    552         }
    553         if (DEBUG) {
    554             Log.d(
    555                     TAG,
    556                     "Found "
    557                             + results.size()
    558                             + " inputs. Elapsed time for searching"
    559                             + " inputs: "
    560                             + (SystemClock.elapsedRealtime() - time)
    561                             + "(msec)");
    562         }
    563         return results;
    564     }
    565 
    566     private String canonicalizeLabel(CharSequence cs) {
    567         Locale locale = mContext.getResources().getConfiguration().locale;
    568         return cs != null ? cs.toString().replaceAll("[ -]", "").toLowerCase(locale) : null;
    569     }
    570 
    571     private SearchResult buildSearchResultForInput(String inputId) {
    572         SearchResult.Builder result = SearchResult.builder();
    573         result.setIntentAction(Intent.ACTION_VIEW);
    574         result.setIntentData(TvContract.buildChannelUriForPassthroughInput(inputId).toString());
    575         return result.build();
    576     }
    577 
    578     @WorkerThread
    579     private class ChannelComparatorWithSameDisplayNumber implements Comparator<SearchResult> {
    580         private final Map<Long, Long> mMaxWatchStartTimeMap = new HashMap<>();
    581 
    582         @Override
    583         public int compare(SearchResult lhs, SearchResult rhs) {
    584             // Show recently watched channel first
    585             Long lhsMaxWatchStartTime = mMaxWatchStartTimeMap.get(lhs.getChannelId());
    586             if (lhsMaxWatchStartTime == null) {
    587                 lhsMaxWatchStartTime = getMaxWatchStartTime(lhs.getChannelId());
    588                 mMaxWatchStartTimeMap.put(lhs.getChannelId(), lhsMaxWatchStartTime);
    589             }
    590             Long rhsMaxWatchStartTime = mMaxWatchStartTimeMap.get(rhs.getChannelId());
    591             if (rhsMaxWatchStartTime == null) {
    592                 rhsMaxWatchStartTime = getMaxWatchStartTime(rhs.getChannelId());
    593                 mMaxWatchStartTimeMap.put(rhs.getChannelId(), rhsMaxWatchStartTime);
    594             }
    595             if (!Objects.equals(lhsMaxWatchStartTime, rhsMaxWatchStartTime)) {
    596                 return Long.compare(rhsMaxWatchStartTime, lhsMaxWatchStartTime);
    597             }
    598             // Show recently added channel first if there's no watch history.
    599             return Long.compare(rhs.getChannelId(), lhs.getChannelId());
    600         }
    601 
    602         private long getMaxWatchStartTime(long channelId) {
    603             Uri uri = WatchedPrograms.CONTENT_URI;
    604             String[] projections =
    605                     new String[] {
    606                         "MAX("
    607                                 + WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS
    608                                 + ") AS max_watch_start_time"
    609                     };
    610             String selection = WatchedPrograms.COLUMN_CHANNEL_ID + "=?";
    611             String[] selectionArgs = new String[] {Long.toString(channelId)};
    612             try (Cursor c =
    613                     mContentResolver.query(uri, projections, selection, selectionArgs, null)) {
    614                 if (c != null && c.moveToNext()) {
    615                     return c.getLong(0);
    616                 }
    617             }
    618             return -1;
    619         }
    620     }
    621 }
    622