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.app.SearchManager;
20import android.content.ContentProvider;
21import android.content.ContentValues;
22import android.database.Cursor;
23import android.database.MatrixCursor;
24import android.net.Uri;
25import android.os.SystemClock;
26import android.support.annotation.NonNull;
27import android.support.annotation.VisibleForTesting;
28import android.text.TextUtils;
29import android.util.Log;
30
31import com.android.tv.TvApplication;
32import com.android.tv.common.SoftPreconditions;
33import com.android.tv.common.TvCommonUtils;
34import com.android.tv.perf.EventNames;
35import com.android.tv.perf.PerformanceMonitor;
36import com.android.tv.perf.TimerEvent;
37import com.android.tv.util.PermissionUtils;
38import com.android.tv.util.TvUriMatcher;
39
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.List;
43
44public class LocalSearchProvider extends ContentProvider {
45    private static final String TAG = "LocalSearchProvider";
46    private static final boolean DEBUG = false;
47
48    /** The authority for LocalSearchProvider. */
49    public static final String AUTHORITY = "com.android.tv.search";
50
51    public static final int PROGRESS_PERCENTAGE_HIDE = -1;
52
53    // TODO: Remove this once added to the SearchManager.
54    private static final String SUGGEST_COLUMN_PROGRESS_BAR_PERCENTAGE = "progress_bar_percentage";
55
56    private static final String[] SEARCHABLE_COLUMNS = new String[] {
57        SearchManager.SUGGEST_COLUMN_TEXT_1,
58        SearchManager.SUGGEST_COLUMN_TEXT_2,
59        SearchManager.SUGGEST_COLUMN_RESULT_CARD_IMAGE,
60        SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
61        SearchManager.SUGGEST_COLUMN_INTENT_DATA,
62        SearchManager.SUGGEST_COLUMN_CONTENT_TYPE,
63        SearchManager.SUGGEST_COLUMN_IS_LIVE,
64        SearchManager.SUGGEST_COLUMN_VIDEO_WIDTH,
65        SearchManager.SUGGEST_COLUMN_VIDEO_HEIGHT,
66        SearchManager.SUGGEST_COLUMN_DURATION,
67        SUGGEST_COLUMN_PROGRESS_BAR_PERCENTAGE
68    };
69
70    private static final String EXPECTED_PATH_PREFIX = "/" + SearchManager.SUGGEST_URI_PATH_QUERY;
71    static final String SUGGEST_PARAMETER_ACTION = "action";
72    // The launcher passes 10 as a 'limit' parameter by default.
73    @VisibleForTesting
74    static final int DEFAULT_SEARCH_LIMIT = 10;
75    @VisibleForTesting
76    static final int DEFAULT_SEARCH_ACTION = SearchInterface.ACTION_TYPE_AMBIGUOUS;
77
78    private static final String NO_LIVE_CONTENTS = "0";
79    private static final String LIVE_CONTENTS = "1";
80
81    private PerformanceMonitor mPerformanceMonitor;
82
83    /** Used only for testing */
84    private SearchInterface mSearchInterface;
85
86    @Override
87    public boolean onCreate() {
88        mPerformanceMonitor = TvApplication.getSingletons(getContext()).getPerformanceMonitor();
89        return true;
90    }
91
92    @VisibleForTesting
93    void setSearchInterface(SearchInterface searchInterface) {
94        SoftPreconditions.checkState(TvCommonUtils.isRunningInTest());
95        mSearchInterface = searchInterface;
96    }
97
98    @Override
99    public Cursor query(@NonNull Uri uri, String[] projection, String selection,
100            String[] selectionArgs, String sortOrder) {
101        if (TvUriMatcher.match(uri) != TvUriMatcher.MATCH_ON_DEVICE_SEARCH) {
102            throw new IllegalArgumentException("Unknown URI: " + uri);
103        }
104        TimerEvent queryTimer = mPerformanceMonitor.startTimer();
105        if (DEBUG) {
106            Log.d(TAG, "query(" + uri + ", " + Arrays.toString(projection) + ", " + selection + ", "
107                    + Arrays.toString(selectionArgs) + ", " + sortOrder + ")");
108        }
109        long time = SystemClock.elapsedRealtime();
110        SearchInterface search = mSearchInterface;
111        if (search == null) {
112            if (PermissionUtils.hasAccessAllEpg(getContext())) {
113                if (DEBUG) Log.d(TAG, "Performing TV Provider search.");
114                search = new TvProviderSearch(getContext());
115            } else {
116                if (DEBUG) Log.d(TAG, "Performing Data Manager search.");
117                search = new DataManagerSearch(getContext());
118            }
119        }
120        String query = uri.getLastPathSegment();
121        int limit = getQueryParamater(uri, SearchManager.SUGGEST_PARAMETER_LIMIT,
122                DEFAULT_SEARCH_LIMIT);
123        if (limit <= 0) {
124            limit = DEFAULT_SEARCH_LIMIT;
125        }
126        int action = getQueryParamater(uri, SUGGEST_PARAMETER_ACTION, DEFAULT_SEARCH_ACTION);
127        if (action < SearchInterface.ACTION_TYPE_START
128                || action > SearchInterface.ACTION_TYPE_END) {
129            action = DEFAULT_SEARCH_ACTION;
130        }
131        List<SearchResult> results = new ArrayList<>();
132        if (!TextUtils.isEmpty(query)) {
133            results.addAll(search.search(query, limit, action));
134        }
135        Cursor c = createSuggestionsCursor(results);
136        if (DEBUG) {
137            Log.d(TAG, "Elapsed time(count=" + c.getCount() + "): "
138                    + (SystemClock.elapsedRealtime() - time) + "(msec)");
139        }
140        mPerformanceMonitor.stopTimer(queryTimer, EventNames.ON_DEVICE_SEARCH);
141        return c;
142    }
143
144    private int getQueryParamater(Uri uri, String key, int defaultValue) {
145        try {
146            return Integer.parseInt(uri.getQueryParameter(key));
147        } catch (NumberFormatException | UnsupportedOperationException e) {
148            // Ignore the exceptions
149        }
150        return defaultValue;
151    }
152
153    private Cursor createSuggestionsCursor(List<SearchResult> results) {
154        MatrixCursor cursor = new MatrixCursor(SEARCHABLE_COLUMNS, results.size());
155        List<String> row = new ArrayList<>(SEARCHABLE_COLUMNS.length);
156
157        int index = 0;
158        for (SearchResult result : results) {
159            row.clear();
160            row.add(result.title);
161            row.add(result.description);
162            row.add(result.imageUri);
163            row.add(result.intentAction);
164            row.add(result.intentData);
165            row.add(result.contentType);
166            row.add(result.isLive ? LIVE_CONTENTS : NO_LIVE_CONTENTS);
167            row.add(result.videoWidth == 0 ? null : String.valueOf(result.videoWidth));
168            row.add(result.videoHeight == 0 ? null : String.valueOf(result.videoHeight));
169            row.add(result.duration == 0 ? null : String.valueOf(result.duration));
170            row.add(String.valueOf(result.progressPercentage));
171            cursor.addRow(row);
172            if (DEBUG) Log.d(TAG, "Result[" + (++index) + "]: " + result);
173        }
174        return cursor;
175    }
176
177    @Override
178    public String getType(Uri uri) {
179        if (!checkUriCorrect(uri)) return null;
180        return SearchManager.SUGGEST_MIME_TYPE;
181    }
182
183    private static boolean checkUriCorrect(Uri uri) {
184        return uri != null && uri.getPath().startsWith(EXPECTED_PATH_PREFIX);
185    }
186
187    @Override
188    public Uri insert(Uri uri, ContentValues values) {
189        throw new UnsupportedOperationException();
190    }
191
192    @Override
193    public int delete(Uri uri, String selection, String[] selectionArgs) {
194        throw new UnsupportedOperationException();
195    }
196
197    @Override
198    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
199        throw new UnsupportedOperationException();
200    }
201
202    /**
203     * A placeholder to a search result.
204     */
205    public static class SearchResult {
206        public long channelId;
207        public String channelNumber;
208        public String title;
209        public String description;
210        public String imageUri;
211        public String intentAction;
212        public String intentData;
213        public String contentType;
214        public boolean isLive;
215        public int videoWidth;
216        public int videoHeight;
217        public long duration;
218        public int progressPercentage;
219
220        @Override
221        public String toString() {
222            return "SearchResult{channelId=" + channelId +
223                    ", channelNumber=" + channelNumber +
224                    ", title=" + title +
225                    ", description=" + description +
226                    ", imageUri=" + imageUri +
227                    ", intentAction=" + intentAction +
228                    ", intentData=" + intentData +
229                    ", contentType=" + contentType +
230                    ", isLive=" + isLive +
231                    ", videoWidth=" + videoWidth +
232                    ", videoHeight=" + videoHeight +
233                    ", duration=" + duration +
234                    ", progressPercentage=" + progressPercentage +
235                    "}";
236        }
237    }
238}