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.ContentProvider; 21 import android.content.ContentValues; 22 import android.database.Cursor; 23 import android.database.MatrixCursor; 24 import android.net.Uri; 25 import android.os.SystemClock; 26 import android.support.annotation.NonNull; 27 import android.support.annotation.VisibleForTesting; 28 import android.text.TextUtils; 29 import android.util.Log; 30 31 import com.android.tv.TvApplication; 32 import com.android.tv.common.SoftPreconditions; 33 import com.android.tv.common.TvCommonUtils; 34 import com.android.tv.perf.EventNames; 35 import com.android.tv.perf.PerformanceMonitor; 36 import com.android.tv.perf.TimerEvent; 37 import com.android.tv.util.PermissionUtils; 38 import com.android.tv.util.TvUriMatcher; 39 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.List; 43 44 public 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 }