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.app.SearchManager;
20 import android.content.ContentValues;
21 import android.database.Cursor;
22 import android.database.MatrixCursor;
23 import android.net.Uri;
24 import android.os.SystemClock;
25 import android.support.annotation.NonNull;
26 import android.support.annotation.Nullable;
27 import android.support.annotation.VisibleForTesting;
28 import android.text.TextUtils;
29 import android.util.Log;
30 
31 import com.android.tv.common.CommonConstants;
32 import com.android.tv.common.SoftPreconditions;
33 import com.android.tv.common.dagger.init.SafePreDaggerInitializer;
34 import com.android.tv.common.util.CommonUtils;
35 import com.android.tv.common.util.PermissionUtils;
36 import com.android.tv.perf.EventNames;
37 import com.android.tv.perf.PerformanceMonitor;
38 import com.android.tv.perf.TimerEvent;
39 import com.android.tv.util.TvUriMatcher;
40 
41 import com.google.auto.value.AutoValue;
42 
43 import dagger.android.ContributesAndroidInjector;
44 import dagger.android.DaggerContentProvider;
45 
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.List;
49 
50 import javax.inject.Inject;
51 
52 /** Content provider for local search */
53 public class LocalSearchProvider extends DaggerContentProvider {
54     private static final String TAG = "LocalSearchProvider";
55     private static final boolean DEBUG = false;
56 
57     /** The authority for LocalSearchProvider. */
58     public static final String AUTHORITY = CommonConstants.BASE_PACKAGE + ".search";
59 
60     // TODO: Remove this once added to the SearchManager.
61     private static final String SUGGEST_COLUMN_PROGRESS_BAR_PERCENTAGE = "progress_bar_percentage";
62 
63     private static final String[] SEARCHABLE_COLUMNS =
64             new String[] {
65                 SearchManager.SUGGEST_COLUMN_TEXT_1,
66                 SearchManager.SUGGEST_COLUMN_TEXT_2,
67                 SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE,
68                 SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
69                 SearchManager.SUGGEST_COLUMN_INTENT_DATA,
70                 SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA,
71                 SearchManager.SUGGEST_COLUMN_CONTENT_TYPE,
72                 SearchManager.SUGGEST_COLUMN_IS_LIVE,
73                 SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH,
74                 SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT,
75                 SearchManager.SUGGEST_COLUMN_DURATION,
76                 SUGGEST_COLUMN_PROGRESS_BAR_PERCENTAGE
77             };
78 
79     private static final String EXPECTED_PATH_PREFIX = "/" + SearchManager.SUGGEST_URI_PATH_QUERY;
80     static final String SUGGEST_PARAMETER_ACTION = "action";
81     // The launcher passes 10 as a 'limit' parameter by default.
82     @VisibleForTesting static final int DEFAULT_SEARCH_LIMIT = 10;
83 
84     @VisibleForTesting
85     static final int DEFAULT_SEARCH_ACTION = SearchInterface.ACTION_TYPE_AMBIGUOUS;
86 
87     private static final String NO_LIVE_CONTENTS = "0";
88     private static final String LIVE_CONTENTS = "1";
89 
90     @Inject PerformanceMonitor mPerformanceMonitor;
91 
92     /** Used only for testing */
93     private SearchInterface mSearchInterface;
94 
95     @Override
onCreate()96     public boolean onCreate() {
97         SafePreDaggerInitializer.init(getContext());
98         if (!super.onCreate()) {
99             Log.e(TAG, "LocalSearchProvider.onCreate() failed.");
100             return false;
101         }
102         return true;
103     }
104 
105     @VisibleForTesting
setSearchInterface(SearchInterface searchInterface)106     void setSearchInterface(SearchInterface searchInterface) {
107         SoftPreconditions.checkState(CommonUtils.isRunningInTest());
108         mSearchInterface = searchInterface;
109     }
110 
111     @Override
query( @onNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)112     public Cursor query(
113             @NonNull Uri uri,
114             String[] projection,
115             String selection,
116             String[] selectionArgs,
117             String sortOrder) {
118         if (TvUriMatcher.match(uri) != TvUriMatcher.MATCH_ON_DEVICE_SEARCH) {
119             throw new IllegalArgumentException("Unknown URI: " + uri);
120         }
121         TimerEvent queryTimer = mPerformanceMonitor.startTimer();
122         if (DEBUG) {
123             Log.d(
124                     TAG,
125                     "query("
126                             + uri
127                             + ", "
128                             + Arrays.toString(projection)
129                             + ", "
130                             + selection
131                             + ", "
132                             + Arrays.toString(selectionArgs)
133                             + ", "
134                             + sortOrder
135                             + ")");
136         }
137         long time = SystemClock.elapsedRealtime();
138         SearchInterface search = mSearchInterface;
139         if (search == null) {
140             if (PermissionUtils.hasAccessAllEpg(getContext())) {
141                 if (DEBUG) Log.d(TAG, "Performing TV Provider search.");
142                 search = new TvProviderSearch(getContext());
143             } else {
144                 if (DEBUG) Log.d(TAG, "Performing Data Manager search.");
145                 search = new DataManagerSearch(getContext());
146             }
147         }
148         String query = uri.getLastPathSegment();
149         int limit =
150                 getQueryParamater(uri, SearchManager.SUGGEST_PARAMETER_LIMIT, DEFAULT_SEARCH_LIMIT);
151         if (limit <= 0) {
152             limit = DEFAULT_SEARCH_LIMIT;
153         }
154         int action = getQueryParamater(uri, SUGGEST_PARAMETER_ACTION, DEFAULT_SEARCH_ACTION);
155         if (action < SearchInterface.ACTION_TYPE_START
156                 || action > SearchInterface.ACTION_TYPE_END) {
157             action = DEFAULT_SEARCH_ACTION;
158         }
159         List<SearchResult> results = new ArrayList<>();
160         if (!TextUtils.isEmpty(query)) {
161             results.addAll(search.search(query, limit, action));
162         }
163         Cursor c = createSuggestionsCursor(results);
164         if (DEBUG) {
165             Log.d(
166                     TAG,
167                     "Elapsed time(count="
168                             + c.getCount()
169                             + "): "
170                             + (SystemClock.elapsedRealtime() - time)
171                             + "(msec)");
172         }
173         mPerformanceMonitor.stopTimer(queryTimer, EventNames.ON_DEVICE_SEARCH);
174         return c;
175     }
176 
getQueryParamater(Uri uri, String key, int defaultValue)177     private int getQueryParamater(Uri uri, String key, int defaultValue) {
178         try {
179             return Integer.parseInt(uri.getQueryParameter(key));
180         } catch (NumberFormatException | UnsupportedOperationException e) {
181             // Ignore the exceptions
182         }
183         return defaultValue;
184     }
185 
createSuggestionsCursor(List<SearchResult> results)186     private Cursor createSuggestionsCursor(List<SearchResult> results) {
187         MatrixCursor cursor = new MatrixCursor(SEARCHABLE_COLUMNS, results.size());
188         List<String> row = new ArrayList<>(SEARCHABLE_COLUMNS.length);
189 
190         int index = 0;
191         for (SearchResult result : results) {
192             row.clear();
193             row.add(result.getTitle());
194             row.add(result.getDescription());
195             row.add(result.getImageUri());
196             row.add(result.getIntentAction());
197             row.add(result.getIntentData());
198             row.add(result.getIntentExtraData());
199             row.add(result.getContentType());
200             row.add(result.getIsLive() ? LIVE_CONTENTS : NO_LIVE_CONTENTS);
201             row.add(result.getVideoWidth() == 0 ? null : String.valueOf(result.getVideoWidth()));
202             row.add(result.getVideoHeight() == 0 ? null : String.valueOf(result.getVideoHeight()));
203             row.add(result.getDuration() == 0 ? null : String.valueOf(result.getDuration()));
204             row.add(String.valueOf(result.getProgressPercentage()));
205             cursor.addRow(row);
206             if (DEBUG) Log.d(TAG, "Result[" + (++index) + "]: " + result);
207         }
208         return cursor;
209     }
210 
211     @Override
getType(Uri uri)212     public String getType(Uri uri) {
213         if (!checkUriCorrect(uri)) return null;
214         return SearchManager.SUGGEST_MIME_TYPE;
215     }
216 
checkUriCorrect(Uri uri)217     private static boolean checkUriCorrect(Uri uri) {
218         return uri != null && uri.getPath().startsWith(EXPECTED_PATH_PREFIX);
219     }
220 
221     @Override
insert(Uri uri, ContentValues values)222     public Uri insert(Uri uri, ContentValues values) {
223         throw new UnsupportedOperationException();
224     }
225 
226     @Override
delete(Uri uri, String selection, String[] selectionArgs)227     public int delete(Uri uri, String selection, String[] selectionArgs) {
228         throw new UnsupportedOperationException();
229     }
230 
231     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)232     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
233         throw new UnsupportedOperationException();
234     }
235 
236     /** Module for {@link LocalSearchProvider} */
237     @dagger.Module
238     public abstract static class Module {
239         @ContributesAndroidInjector
contributesLocalSearchProviderInjector()240         abstract LocalSearchProvider contributesLocalSearchProviderInjector();
241     }
242 
243     /** A placeholder to a search result. */
244     @AutoValue
245     public abstract static class SearchResult {
builder()246         public static Builder builder() {
247             // primitive fields cannot be nullable. Set to default;
248             return new AutoValue_LocalSearchProvider_SearchResult.Builder()
249                     .setChannelId(0)
250                     .setIsLive(false)
251                     .setVideoWidth(0)
252                     .setVideoHeight(0)
253                     .setDuration(0)
254                     .setProgressPercentage(0);
255         }
256 
toBuilder()257         public abstract Builder toBuilder();
258 
259         @AutoValue.Builder
260         abstract static class Builder {
setChannelId(long value)261             abstract Builder setChannelId(long value);
262 
setChannelNumber(String value)263             abstract Builder setChannelNumber(String value);
264 
setTitle(String value)265             abstract Builder setTitle(String value);
266 
setDescription(String value)267             abstract Builder setDescription(String value);
268 
setImageUri(String value)269             abstract Builder setImageUri(String value);
270 
setIntentAction(String value)271             abstract Builder setIntentAction(String value);
272 
setIntentData(String value)273             abstract Builder setIntentData(String value);
274 
setIntentExtraData(String value)275             abstract Builder setIntentExtraData(String value);
276 
setContentType(String value)277             abstract Builder setContentType(String value);
278 
setIsLive(boolean value)279             abstract Builder setIsLive(boolean value);
280 
setVideoWidth(int value)281             abstract Builder setVideoWidth(int value);
282 
setVideoHeight(int value)283             abstract Builder setVideoHeight(int value);
284 
setDuration(long value)285             abstract Builder setDuration(long value);
286 
setProgressPercentage(int value)287             abstract Builder setProgressPercentage(int value);
288 
build()289             abstract SearchResult build();
290         }
291 
getChannelId()292         abstract long getChannelId();
293 
294         @Nullable
getChannelNumber()295         abstract String getChannelNumber();
296 
297         @Nullable
getTitle()298         abstract String getTitle();
299 
300         @Nullable
getDescription()301         abstract String getDescription();
302 
303         @Nullable
getImageUri()304         abstract String getImageUri();
305 
306         @Nullable
getIntentAction()307         abstract String getIntentAction();
308 
309         @Nullable
getIntentData()310         abstract String getIntentData();
311 
312         @Nullable
getIntentExtraData()313         abstract String getIntentExtraData();
314 
315         @Nullable
getContentType()316         abstract String getContentType();
317 
getIsLive()318         abstract boolean getIsLive();
319 
getVideoWidth()320         abstract int getVideoWidth();
321 
getVideoHeight()322         abstract int getVideoHeight();
323 
getDuration()324         abstract long getDuration();
325 
getProgressPercentage()326         abstract int getProgressPercentage();
327     }
328 }
329