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