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