TvProviderSearch.java revision 8294723aefc8341646c66f378a02098db95716de
1/*
2 * Copyright (C) 2014 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.ContentUris;
20import android.content.Context;
21import android.content.Intent;
22import android.database.Cursor;
23import android.media.tv.TvContract.Channels;
24import android.media.tv.TvContract.Programs;
25import android.net.Uri;
26
27import com.android.internal.util.Preconditions;
28
29import java.util.ArrayList;
30import java.util.HashMap;
31import java.util.HashSet;
32import java.util.List;
33import java.util.Map;
34import java.util.Set;
35
36public class TvProviderSearch {
37    public static List<SearchResult> search(Context context, String query, int limit) {
38        List<SearchResult> results = new ArrayList<SearchResult>();
39        results.addAll(searchChannels(context, query, new String[] {
40                Channels.COLUMN_DISPLAY_NAME,
41                Channels.COLUMN_DESCRIPTION
42        }, limit));
43        if (results.size() >= limit) {
44            return results;
45        }
46
47        Set<Long> previousResults = getChannelIdSet(results);
48        limit -= results.size();
49        results.addAll(searchPrograms(context, query, new String[] {
50                Programs.COLUMN_TITLE,
51                Programs.COLUMN_SHORT_DESCRIPTION
52        }, previousResults, limit));
53        return results;
54    }
55
56    private static Set<Long> getChannelIdSet(List<SearchResult> results) {
57        Set<Long> channelIdSet = new HashSet<Long>();
58        for (SearchResult sr : results) {
59            channelIdSet.add(sr.getChannelId());
60        }
61        return channelIdSet;
62    }
63
64    private static List<SearchResult> searchChannels(Context context, String query,
65            String[] columnNames, int limit) {
66        Preconditions.checkState(columnNames != null && columnNames.length > 0);
67
68        String[] projection = {
69                Channels._ID,
70                Channels.COLUMN_DISPLAY_NAME,
71                Channels.COLUMN_DESCRIPTION,
72        };
73
74        StringBuilder sb = new StringBuilder();
75        sb.append(Channels.COLUMN_BROWSABLE).append("=1 AND ");
76        sb.append(Channels.COLUMN_SEARCHABLE).append("=1 AND (");
77        sb.append(columnNames[0]).append(" like ?");
78        for (int i = 1; i < columnNames.length; ++i) {
79            sb.append(" OR ").append(columnNames[i]).append(" like ?");
80        }
81        sb.append(")");
82        String selection = sb.toString();
83
84        String selectionArg = "%" + query + "%";
85        String[] selectionArgs = new String[columnNames.length];
86        for (int i = 0; i < selectionArgs.length; ++i) {
87            selectionArgs[i] = selectionArg;
88        }
89
90        return search(context, Channels.CONTENT_URI, projection, selection, selectionArgs, limit,
91                null);
92    }
93
94    private static List<SearchResult> searchPrograms(final Context context, String query,
95            String[] columnNames, final Set<Long> previousResults, int limit) {
96        Preconditions.checkState(columnNames != null && columnNames.length > 0);
97
98        String[] projection = {
99                Programs.COLUMN_CHANNEL_ID,
100                Programs.COLUMN_TITLE,
101                Programs.COLUMN_SHORT_DESCRIPTION,
102        };
103
104        StringBuilder sb = new StringBuilder();
105        // Search among the programs which are now being on the air.
106        sb.append(Programs.COLUMN_START_TIME_UTC_MILLIS).append("<=? AND ");
107        sb.append(Programs.COLUMN_END_TIME_UTC_MILLIS).append(">=? AND (");
108        sb.append(columnNames[0]).append(" like ?");
109        for (int i = 1; i < columnNames.length; ++i) {
110            sb.append(" OR ").append(columnNames[0]).append(" like ?");
111        }
112        sb.append(")");
113        String selection = sb.toString();
114        String selectionArg = "%" + query + "%";
115        String[] selectionArgs = new String[columnNames.length+2];
116        selectionArgs[0] = selectionArgs[1] = String.valueOf(System.currentTimeMillis());
117        for (int i = 2; i < selectionArgs.length; ++i) {
118            selectionArgs[i] = selectionArg;
119        }
120
121        return search(context, Programs.CONTENT_URI, projection, selection, selectionArgs, limit,
122                new ResultFilter() {
123                    private Map<Long, Boolean> searchableMap = new HashMap<Long, Boolean>();
124
125                    @Override
126                    public boolean filter(Cursor c) {
127                        long id = c.getLong(0);
128                        // Filter out the program whose channel is already searched.
129                        if (previousResults.contains(id)) {
130                            return false;
131                        }
132                        // The channel is cached.
133                        Boolean isSearchable = searchableMap.get(id);
134                        if (isSearchable != null) {
135                            return isSearchable;
136                        }
137
138                        // Don't know whether the channel is searchable or not.
139                        String selection = Channels._ID + "=? AND "
140                                + Channels.COLUMN_BROWSABLE + "=1 AND "
141                                + Channels.COLUMN_SEARCHABLE + "=1";
142                        Cursor cursor = null;
143                        try {
144                            // Don't need to fetch all the columns.
145                            cursor = context.getContentResolver().query(Channels.CONTENT_URI,
146                                    new String[] { Channels._ID }, selection,
147                                    new String[] { String.valueOf(id) }, null);
148                            boolean isSearchableChannel = cursor != null && cursor.getCount() > 0;
149                            searchableMap.put(id, isSearchableChannel);
150                            return isSearchableChannel;
151                        } finally {
152                            if (cursor != null) {
153                                cursor.close();
154                            }
155                        }
156                    }
157        });
158    }
159
160    private static List<SearchResult> search(Context context, Uri uri, String[] projection,
161            String selection, String[] selectionArgs, int limit, ResultFilter resultFilter) {
162        List<SearchResult> results = new ArrayList<SearchResult>();
163
164        Cursor cursor = null;
165        try {
166            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
167                    null);
168            if (cursor != null) {
169                // TODO: Need to add image when available.
170                int count = 0;
171                while (cursor.moveToNext()) {
172                    if (resultFilter != null && !resultFilter.filter(cursor)) {
173                        continue;
174                    }
175
176                    long id = cursor.getLong(0);
177                    String title = cursor.getString(1);
178                    String description = cursor.getString(2);
179
180                    SearchResult result = SearchResult.builder()
181                            .setChannelId(id)
182                            .setTitle(title)
183                            .setDescription(description)
184                            .setIntentAction(Intent.ACTION_VIEW)
185                            .setIntentData(ContentUris.withAppendedId(Channels.CONTENT_URI, id)
186                                    .toString())
187                            .build();
188                    results.add(result);
189
190                    if (++count >= limit) {
191                        break;
192                    }
193                }
194            }
195        } finally {
196            if (cursor != null) {
197                cursor.close();
198            }
199        }
200
201        return results;
202    }
203
204    private interface ResultFilter {
205        boolean filter(Cursor c);
206    }
207}
208