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