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