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