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