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.Context;
     20 import android.content.Intent;
     21 import android.media.tv.TvContentRating;
     22 import android.media.tv.TvContract;
     23 import android.media.tv.TvContract.Programs;
     24 import android.media.tv.TvInputManager;
     25 import android.os.SystemClock;
     26 import android.support.annotation.MainThread;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 import com.android.tv.TvSingletons;
     30 import com.android.tv.data.ChannelDataManager;
     31 import com.android.tv.data.Program;
     32 import com.android.tv.data.ProgramDataManager;
     33 import com.android.tv.data.api.Channel;
     34 import com.android.tv.search.LocalSearchProvider.SearchResult;
     35 import com.android.tv.util.MainThreadExecutor;
     36 import com.android.tv.util.Utils;
     37 import java.util.ArrayList;
     38 import java.util.Collections;
     39 import java.util.HashSet;
     40 import java.util.List;
     41 import java.util.Set;
     42 import java.util.concurrent.Callable;
     43 import java.util.concurrent.ExecutionException;
     44 import java.util.concurrent.Future;
     45 
     46 /**
     47  * An implementation of {@link SearchInterface} to search query from {@link ChannelDataManager} and
     48  * {@link ProgramDataManager}.
     49  */
     50 public class DataManagerSearch implements SearchInterface {
     51     private static final String TAG = "DataManagerSearch";
     52     private static final boolean DEBUG = false;
     53 
     54     private final Context mContext;
     55     private final TvInputManager mTvInputManager;
     56     private final ChannelDataManager mChannelDataManager;
     57     private final ProgramDataManager mProgramDataManager;
     58 
     59     DataManagerSearch(Context context) {
     60         mContext = context;
     61         mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
     62         TvSingletons tvSingletons = TvSingletons.getSingletons(context);
     63         mChannelDataManager = tvSingletons.getChannelDataManager();
     64         mProgramDataManager = tvSingletons.getProgramDataManager();
     65     }
     66 
     67     @Override
     68     public List<SearchResult> search(final String query, final int limit, final int action) {
     69         Future<List<SearchResult>> future =
     70                 MainThreadExecutor.getInstance()
     71                         .submit(
     72                                 new Callable<List<SearchResult>>() {
     73                                     @Override
     74                                     public List<SearchResult> call() throws Exception {
     75                                         return searchFromDataManagers(query, limit, action);
     76                                     }
     77                                 });
     78 
     79         try {
     80             return future.get();
     81         } catch (InterruptedException e) {
     82             Thread.interrupted();
     83             return Collections.EMPTY_LIST;
     84         } catch (ExecutionException e) {
     85             Log.w(TAG, "Error searching for " + query, e);
     86             return Collections.EMPTY_LIST;
     87         }
     88     }
     89 
     90     @MainThread
     91     private List<SearchResult> searchFromDataManagers(String query, int limit, int action) {
     92         // TODO(b/72499165): add a test.
     93         List<SearchResult> results = new ArrayList<>();
     94         if (!mChannelDataManager.isDbLoadFinished()) {
     95             return results;
     96         }
     97         if (action == ACTION_TYPE_SWITCH_CHANNEL || action == ACTION_TYPE_SWITCH_INPUT) {
     98             // Voice search query should be handled by the a system TV app.
     99             return results;
    100         }
    101         if (DEBUG) Log.d(TAG, "Searching channels: '" + query + "'");
    102         long time = SystemClock.elapsedRealtime();
    103         Set<Long> channelsFound = new HashSet<>();
    104         List<Channel> channelList = mChannelDataManager.getBrowsableChannelList();
    105         query = query.toLowerCase();
    106         if (TextUtils.isDigitsOnly(query)) {
    107             for (Channel channel : channelList) {
    108                 if (channelsFound.contains(channel.getId())) {
    109                     continue;
    110                 }
    111                 if (contains(channel.getDisplayNumber(), query)) {
    112                     addResult(results, channelsFound, channel, null);
    113                 }
    114                 if (results.size() >= limit) {
    115                     if (DEBUG) {
    116                         Log.d(
    117                                 TAG,
    118                                 "Found "
    119                                         + results.size()
    120                                         + " channels. Elapsed time for"
    121                                         + " searching channels: "
    122                                         + (SystemClock.elapsedRealtime() - time)
    123                                         + "(msec)");
    124                     }
    125                     return results;
    126                 }
    127             }
    128             // TODO: recently watched channels may have higher priority.
    129         }
    130         for (Channel channel : channelList) {
    131             if (channelsFound.contains(channel.getId())) {
    132                 continue;
    133             }
    134             if (contains(channel.getDisplayName(), query)
    135                     || contains(channel.getDescription(), query)) {
    136                 addResult(results, channelsFound, channel, null);
    137             }
    138             if (results.size() >= limit) {
    139                 if (DEBUG) {
    140                     Log.d(
    141                             TAG,
    142                             "Found "
    143                                     + results.size()
    144                                     + " channels. Elapsed time for"
    145                                     + " searching channels: "
    146                                     + (SystemClock.elapsedRealtime() - time)
    147                                     + "(msec)");
    148                 }
    149                 return results;
    150             }
    151         }
    152         if (DEBUG) {
    153             Log.d(
    154                     TAG,
    155                     "Found "
    156                             + results.size()
    157                             + " channels. Elapsed time for"
    158                             + " searching channels: "
    159                             + (SystemClock.elapsedRealtime() - time)
    160                             + "(msec)");
    161         }
    162         int channelResult = results.size();
    163         if (DEBUG) Log.d(TAG, "Searching programs: '" + query + "'");
    164         time = SystemClock.elapsedRealtime();
    165         for (Channel channel : channelList) {
    166             if (channelsFound.contains(channel.getId())) {
    167                 continue;
    168             }
    169             Program program = mProgramDataManager.getCurrentProgram(channel.getId());
    170             if (program == null) {
    171                 continue;
    172             }
    173             if (contains(program.getTitle(), query)
    174                     && !isRatingBlocked(program.getContentRatings())) {
    175                 addResult(results, channelsFound, channel, program);
    176             }
    177             if (results.size() >= limit) {
    178                 if (DEBUG) {
    179                     Log.d(
    180                             TAG,
    181                             "Found "
    182                                     + (results.size() - channelResult)
    183                                     + " programs. Elapsed"
    184                                     + " time for searching programs: "
    185                                     + (SystemClock.elapsedRealtime() - time)
    186                                     + "(msec)");
    187                 }
    188                 return results;
    189             }
    190         }
    191         for (Channel channel : channelList) {
    192             if (channelsFound.contains(channel.getId())) {
    193                 continue;
    194             }
    195             Program program = mProgramDataManager.getCurrentProgram(channel.getId());
    196             if (program == null) {
    197                 continue;
    198             }
    199             if (contains(program.getDescription(), query)
    200                     && !isRatingBlocked(program.getContentRatings())) {
    201                 addResult(results, channelsFound, channel, program);
    202             }
    203             if (results.size() >= limit) {
    204                 if (DEBUG) {
    205                     Log.d(
    206                             TAG,
    207                             "Found "
    208                                     + (results.size() - channelResult)
    209                                     + " programs. Elapsed"
    210                                     + " time for searching programs: "
    211                                     + (SystemClock.elapsedRealtime() - time)
    212                                     + "(msec)");
    213                 }
    214                 return results;
    215             }
    216         }
    217         if (DEBUG) {
    218             Log.d(
    219                     TAG,
    220                     "Found "
    221                             + (results.size() - channelResult)
    222                             + " programs. Elapsed time for"
    223                             + " searching programs: "
    224                             + (SystemClock.elapsedRealtime() - time)
    225                             + "(msec)");
    226         }
    227         return results;
    228     }
    229 
    230     // It assumes that query is already lower cases.
    231     private boolean contains(String string, String query) {
    232         return string != null && string.toLowerCase().contains(query);
    233     }
    234 
    235     /** If query is matched to channel, {@code program} should be null. */
    236     private void addResult(
    237             List<SearchResult> results, Set<Long> channelsFound, Channel channel, Program program) {
    238         if (program == null) {
    239             program = mProgramDataManager.getCurrentProgram(channel.getId());
    240             if (program != null && isRatingBlocked(program.getContentRatings())) {
    241                 program = null;
    242             }
    243         }
    244 
    245         SearchResult.Builder result = SearchResult.builder();
    246 
    247         long channelId = channel.getId();
    248         result.setChannelId(channelId);
    249         result.setChannelNumber(channel.getDisplayNumber());
    250         if (program == null) {
    251             result.setTitle(channel.getDisplayName());
    252             result.setDescription(channel.getDescription());
    253             result.setImageUri(TvContract.buildChannelLogoUri(channelId).toString());
    254             result.setIntentAction(Intent.ACTION_VIEW);
    255             result.setIntentData(buildIntentData(channelId));
    256             result.setContentType(Programs.CONTENT_ITEM_TYPE);
    257             result.setIsLive(true);
    258             result.setProgressPercentage(LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE);
    259         } else {
    260             result.setTitle(program.getTitle());
    261             result.setDescription(
    262                     buildProgramDescription(
    263                             channel.getDisplayNumber(),
    264                             channel.getDisplayName(),
    265                             program.getStartTimeUtcMillis(),
    266                             program.getEndTimeUtcMillis()));
    267             result.setImageUri(program.getPosterArtUri());
    268             result.setIntentAction(Intent.ACTION_VIEW);
    269             result.setIntentData(buildIntentData(channelId));
    270             result.setIntentExtraData(TvContract.buildProgramUri(program.getId()).toString());
    271             result.setContentType(Programs.CONTENT_ITEM_TYPE);
    272             result.setIsLive(true);
    273             result.setVideoWidth(program.getVideoWidth());
    274             result.setVideoHeight(program.getVideoHeight());
    275             result.setDuration(program.getDurationMillis());
    276             result.setProgressPercentage(
    277                     getProgressPercentage(
    278                             program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis()));
    279         }
    280         if (DEBUG) {
    281             Log.d(TAG, "Add a result : channel=" + channel + " program=" + program);
    282         }
    283         results.add(result.build());
    284         channelsFound.add(channel.getId());
    285     }
    286 
    287     private String buildProgramDescription(
    288             String channelNumber,
    289             String channelName,
    290             long programStartUtcMillis,
    291             long programEndUtcMillis) {
    292         return Utils.getDurationString(mContext, programStartUtcMillis, programEndUtcMillis, false)
    293                 + System.lineSeparator()
    294                 + channelNumber
    295                 + " "
    296                 + channelName;
    297     }
    298 
    299     private int getProgressPercentage(long startUtcMillis, long endUtcMillis) {
    300         long current = System.currentTimeMillis();
    301         if (startUtcMillis > current || endUtcMillis <= current) {
    302             return LocalSearchProvider.PROGRESS_PERCENTAGE_HIDE;
    303         }
    304         return (int) (100 * (current - startUtcMillis) / (endUtcMillis - startUtcMillis));
    305     }
    306 
    307     private String buildIntentData(long channelId) {
    308         return TvContract.buildChannelUri(channelId).toString();
    309     }
    310 
    311     private boolean isRatingBlocked(TvContentRating[] ratings) {
    312         if (ratings == null
    313                 || ratings.length == 0
    314                 || !mTvInputManager.isParentalControlsEnabled()) {
    315             return false;
    316         }
    317         for (TvContentRating rating : ratings) {
    318             try {
    319                 if (mTvInputManager.isRatingBlocked(rating)) {
    320                     return true;
    321                 }
    322             } catch (IllegalArgumentException e) {
    323                 // Do nothing.
    324             }
    325         }
    326         return false;
    327     }
    328 }
    329