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