DataManagerSearch.java revision 0cc0713c1bf8027642987b750b80217569d2932a
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
17package com.android.tv.search;
18
19import android.content.Context;
20import android.content.Intent;
21import android.media.tv.TvContentRating;
22import android.media.tv.TvContract;
23import android.media.tv.TvContract.Programs;
24import android.media.tv.TvInputManager;
25import android.os.SystemClock;
26import android.support.annotation.MainThread;
27import android.text.TextUtils;
28import android.util.Log;
29import com.android.tv.TvSingletons;
30import com.android.tv.data.ChannelDataManager;
31import com.android.tv.data.Program;
32import com.android.tv.data.ProgramDataManager;
33import com.android.tv.data.api.Channel;
34import com.android.tv.search.LocalSearchProvider.SearchResult;
35import com.android.tv.util.MainThreadExecutor;
36import com.android.tv.util.Utils;
37import java.util.ArrayList;
38import java.util.Collections;
39import java.util.HashSet;
40import java.util.List;
41import java.util.Set;
42import java.util.concurrent.Callable;
43import java.util.concurrent.ExecutionException;
44import java.util.concurrent.Future;
45
46/**
47 * An implementation of {@link SearchInterface} to search query from {@link ChannelDataManager} and
48 * {@link ProgramDataManager}.
49 */
50public 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