1 /* 2 * Copyright (C) 2017 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 18 package com.android.settings.search; 19 20 import android.content.Context; 21 import android.os.Handler; 22 import android.os.Looper; 23 import android.os.Message; 24 import android.support.annotation.IntDef; 25 import android.support.annotation.MainThread; 26 import android.support.annotation.VisibleForTesting; 27 import android.support.v7.util.DiffUtil; 28 import android.support.v7.widget.RecyclerView; 29 import android.util.ArrayMap; 30 import android.util.Log; 31 import android.util.Pair; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 36 import com.android.settings.R; 37 import com.android.settings.search.ranking.SearchResultsRankerCallback; 38 39 import java.lang.annotation.Retention; 40 import java.lang.annotation.RetentionPolicy; 41 import java.util.ArrayList; 42 import java.util.Collections; 43 import java.util.Comparator; 44 import java.util.HashSet; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Set; 48 import java.util.TreeSet; 49 50 public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder> 51 implements SearchResultsRankerCallback { 52 private static final String TAG = "SearchResultsAdapter"; 53 54 @VisibleForTesting 55 static final String DB_RESULTS_LOADER_KEY = DatabaseResultLoader.class.getName(); 56 57 @VisibleForTesting 58 static final String APP_RESULTS_LOADER_KEY = InstalledAppResultLoader.class.getName(); 59 @VisibleForTesting 60 static final String ACCESSIBILITY_LOADER_KEY = AccessibilityServiceResultLoader.class.getName(); 61 @VisibleForTesting 62 static final String INPUT_DEVICE_LOADER_KEY = InputDeviceResultLoader.class.getName(); 63 64 @VisibleForTesting 65 static final int MSG_RANKING_TIMED_OUT = 1; 66 67 private final SearchFragment mFragment; 68 private final Context mContext; 69 private final List<SearchResult> mSearchResults; 70 private final List<SearchResult> mStaticallyRankedSearchResults; 71 private Map<String, Set<? extends SearchResult>> mResultsMap; 72 private final SearchFeatureProvider mSearchFeatureProvider; 73 private List<Pair<String, Float>> mSearchRankingScores; 74 private Handler mHandler; 75 private boolean mSearchResultsLoaded; 76 private boolean mSearchResultsUpdated; 77 78 @IntDef({DISABLED, PENDING_RESULTS, SUCCEEDED, FAILED, TIMED_OUT}) 79 @Retention(RetentionPolicy.SOURCE) 80 private @interface AsyncRankingState {} 81 @VisibleForTesting 82 static final int DISABLED = 0; 83 @VisibleForTesting 84 static final int PENDING_RESULTS = 1; 85 @VisibleForTesting 86 static final int SUCCEEDED = 2; 87 @VisibleForTesting 88 static final int FAILED = 3; 89 @VisibleForTesting 90 static final int TIMED_OUT = 4; 91 private @AsyncRankingState int mAsyncRankingState; 92 SearchResultsAdapter(SearchFragment fragment, SearchFeatureProvider searchFeatureProvider)93 public SearchResultsAdapter(SearchFragment fragment, 94 SearchFeatureProvider searchFeatureProvider) { 95 mFragment = fragment; 96 mContext = fragment.getContext().getApplicationContext(); 97 mSearchResults = new ArrayList<>(); 98 mResultsMap = new ArrayMap<>(); 99 mSearchRankingScores = new ArrayList<>(); 100 mStaticallyRankedSearchResults = new ArrayList<>(); 101 mSearchFeatureProvider = searchFeatureProvider; 102 103 setHasStableIds(true); 104 } 105 106 @Override onCreateViewHolder(ViewGroup parent, int viewType)107 public SearchViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 108 final Context context = parent.getContext(); 109 final LayoutInflater inflater = LayoutInflater.from(context); 110 final View view; 111 switch (viewType) { 112 case ResultPayload.PayloadType.INTENT: 113 view = inflater.inflate(R.layout.search_intent_item, parent, false); 114 return new IntentSearchViewHolder(view); 115 case ResultPayload.PayloadType.INLINE_SWITCH: 116 // TODO (b/62807132) replace layout InlineSwitchViewHolder and return an 117 // InlineSwitchViewHolder. 118 view = inflater.inflate(R.layout.search_intent_item, parent, false); 119 return new IntentSearchViewHolder(view); 120 case ResultPayload.PayloadType.INLINE_LIST: 121 // TODO (b/62807132) build a inline-list view holder & layout. 122 view = inflater.inflate(R.layout.search_intent_item, parent, false); 123 return new IntentSearchViewHolder(view); 124 case ResultPayload.PayloadType.SAVED_QUERY: 125 view = inflater.inflate(R.layout.search_saved_query_item, parent, false); 126 return new SavedQueryViewHolder(view); 127 default: 128 return null; 129 } 130 } 131 132 @Override onBindViewHolder(SearchViewHolder holder, int position)133 public void onBindViewHolder(SearchViewHolder holder, int position) { 134 holder.onBind(mFragment, mSearchResults.get(position)); 135 } 136 137 @Override getItemId(int position)138 public long getItemId(int position) { 139 return mSearchResults.get(position).stableId; 140 } 141 142 @Override getItemViewType(int position)143 public int getItemViewType(int position) { 144 return mSearchResults.get(position).viewType; 145 } 146 147 @Override getItemCount()148 public int getItemCount() { 149 return mSearchResults.size(); 150 } 151 152 @MainThread 153 @Override onRankingScoresAvailable(List<Pair<String, Float>> searchRankingScores)154 public void onRankingScoresAvailable(List<Pair<String, Float>> searchRankingScores) { 155 // Received the scores, stop the timeout timer. 156 getHandler().removeMessages(MSG_RANKING_TIMED_OUT); 157 if (mAsyncRankingState == PENDING_RESULTS) { 158 mAsyncRankingState = SUCCEEDED; 159 mSearchRankingScores.clear(); 160 mSearchRankingScores.addAll(searchRankingScores); 161 if (canUpdateSearchResults()) { 162 updateSearchResults(); 163 } 164 } else { 165 Log.w(TAG, "Ranking scores became available in invalid state: " + mAsyncRankingState); 166 } 167 } 168 169 @MainThread 170 @Override onRankingFailed()171 public void onRankingFailed() { 172 if (mAsyncRankingState == PENDING_RESULTS) { 173 mAsyncRankingState = FAILED; 174 if (canUpdateSearchResults()) { 175 updateSearchResults(); 176 } 177 } else { 178 Log.w(TAG, "Ranking scores failed in invalid states: " + mAsyncRankingState); 179 } 180 } 181 182 /** 183 * Store the results from each of the loaders to be merged when all loaders are finished. 184 * 185 * @param results the results from the loader. 186 * @param loaderClassName class name of the loader. 187 */ 188 @MainThread addSearchResults(Set<? extends SearchResult> results, String loaderClassName)189 public void addSearchResults(Set<? extends SearchResult> results, String loaderClassName) { 190 if (results == null) { 191 return; 192 } 193 mResultsMap.put(loaderClassName, results); 194 } 195 196 /** 197 * Displays recent searched queries. 198 * 199 * @return The number of saved queries to display 200 */ displaySavedQuery(List<? extends SearchResult> data)201 public int displaySavedQuery(List<? extends SearchResult> data) { 202 clearResults(); 203 mSearchResults.addAll(data); 204 notifyDataSetChanged(); 205 return mSearchResults.size(); 206 } 207 208 /** 209 * Notifies the adapter that all the unsorted results are loaded and now the ladapter can 210 * proceed with ranking the results. 211 */ 212 @MainThread notifyResultsLoaded()213 public void notifyResultsLoaded() { 214 mSearchResultsLoaded = true; 215 // static ranking is skipped only if asyc ranking is already succeeded. 216 if (mAsyncRankingState != SUCCEEDED) { 217 doStaticRanking(); 218 } 219 if (canUpdateSearchResults()) { 220 updateSearchResults(); 221 } 222 } 223 clearResults()224 public void clearResults() { 225 mSearchResults.clear(); 226 mStaticallyRankedSearchResults.clear(); 227 mResultsMap.clear(); 228 notifyDataSetChanged(); 229 } 230 231 @VisibleForTesting getSearchResults()232 public List<SearchResult> getSearchResults() { 233 return mSearchResults; 234 } 235 236 @MainThread initializeSearch(String query)237 public void initializeSearch(String query) { 238 clearResults(); 239 mSearchResultsLoaded = false; 240 mSearchResultsUpdated = false; 241 if (mSearchFeatureProvider.isSmartSearchRankingEnabled(mContext)) { 242 mAsyncRankingState = PENDING_RESULTS; 243 mSearchFeatureProvider.cancelPendingSearchQuery(mContext); 244 final Handler handler = getHandler(); 245 final long timeoutMs = mSearchFeatureProvider.smartSearchRankingTimeoutMs(mContext); 246 handler.sendMessageDelayed( 247 handler.obtainMessage(MSG_RANKING_TIMED_OUT), timeoutMs); 248 mSearchFeatureProvider.querySearchResults(mContext, query, this); 249 } else { 250 mAsyncRankingState = DISABLED; 251 } 252 } 253 getAsyncRankingState()254 @AsyncRankingState int getAsyncRankingState() { 255 return mAsyncRankingState; 256 } 257 258 /** 259 * Merge the results from each of the loaders into one list for the adapter. 260 * Prioritizes results from the local database over installed apps. 261 */ doStaticRanking()262 private void doStaticRanking() { 263 List<? extends SearchResult> databaseResults = 264 getSortedLoadedResults(DB_RESULTS_LOADER_KEY); 265 List<? extends SearchResult> installedAppResults = 266 getSortedLoadedResults(APP_RESULTS_LOADER_KEY); 267 List<? extends SearchResult> accessibilityResults = 268 getSortedLoadedResults(ACCESSIBILITY_LOADER_KEY); 269 List<? extends SearchResult> inputDeviceResults = 270 getSortedLoadedResults(INPUT_DEVICE_LOADER_KEY); 271 272 int dbSize = databaseResults.size(); 273 int appSize = installedAppResults.size(); 274 int a11ySize = accessibilityResults.size(); 275 int inputDeviceSize = inputDeviceResults.size(); 276 int dbIndex = 0; 277 int appIndex = 0; 278 int a11yIndex = 0; 279 int inputDeviceIndex = 0; 280 int rank = SearchResult.TOP_RANK; 281 282 // TODO: We need a helper method to do k-way merge. 283 mStaticallyRankedSearchResults.clear(); 284 while (rank <= SearchResult.BOTTOM_RANK) { 285 while ((dbIndex < dbSize) && (databaseResults.get(dbIndex).rank == rank)) { 286 mStaticallyRankedSearchResults.add(databaseResults.get(dbIndex++)); 287 } 288 while ((appIndex < appSize) && (installedAppResults.get(appIndex).rank == rank)) { 289 mStaticallyRankedSearchResults.add(installedAppResults.get(appIndex++)); 290 } 291 while ((a11yIndex < a11ySize) && (accessibilityResults.get(a11yIndex).rank == rank)) { 292 mStaticallyRankedSearchResults.add(accessibilityResults.get(a11yIndex++)); 293 } 294 while (inputDeviceIndex < inputDeviceSize 295 && inputDeviceResults.get(inputDeviceIndex).rank == rank) { 296 mStaticallyRankedSearchResults.add(inputDeviceResults.get(inputDeviceIndex++)); 297 } 298 rank++; 299 } 300 301 while (dbIndex < dbSize) { 302 mStaticallyRankedSearchResults.add(databaseResults.get(dbIndex++)); 303 } 304 while (appIndex < appSize) { 305 mStaticallyRankedSearchResults.add(installedAppResults.get(appIndex++)); 306 } 307 while(a11yIndex < a11ySize) { 308 mStaticallyRankedSearchResults.add(accessibilityResults.get(a11yIndex++)); 309 } 310 while (inputDeviceIndex < inputDeviceSize) { 311 mStaticallyRankedSearchResults.add(inputDeviceResults.get(inputDeviceIndex++)); 312 } 313 } 314 updateSearchResults()315 private void updateSearchResults() { 316 switch (mAsyncRankingState) { 317 case PENDING_RESULTS: 318 break; 319 case DISABLED: 320 case FAILED: 321 case TIMED_OUT: 322 // When DISABLED or FAILED or TIMED_OUT, we use static ranking results. 323 postSearchResults(mStaticallyRankedSearchResults, false); 324 break; 325 case SUCCEEDED: 326 postSearchResults(doAsyncRanking(), true); 327 break; 328 } 329 } 330 canUpdateSearchResults()331 private boolean canUpdateSearchResults() { 332 // Results are not updated yet and db results are loaded and we are not waiting on async 333 // ranking scores. 334 return !mSearchResultsUpdated 335 && mSearchResultsLoaded 336 && mAsyncRankingState != PENDING_RESULTS; 337 } 338 339 @VisibleForTesting doAsyncRanking()340 List<SearchResult> doAsyncRanking() { 341 Set<? extends SearchResult> databaseResults = 342 getUnsortedLoadedResults(DB_RESULTS_LOADER_KEY); 343 List<? extends SearchResult> installedAppResults = 344 getSortedLoadedResults(APP_RESULTS_LOADER_KEY); 345 List<? extends SearchResult> accessibilityResults = 346 getSortedLoadedResults(ACCESSIBILITY_LOADER_KEY); 347 List<? extends SearchResult> inputDeviceResults = 348 getSortedLoadedResults(INPUT_DEVICE_LOADER_KEY); 349 int dbSize = databaseResults.size(); 350 int appSize = installedAppResults.size(); 351 int a11ySize = accessibilityResults.size(); 352 int inputDeviceSize = inputDeviceResults.size(); 353 354 final List<SearchResult> asyncRankingResults = new ArrayList<>( 355 dbSize + appSize + a11ySize + inputDeviceSize); 356 TreeSet<SearchResult> dbResultsSortedByScores = new TreeSet<>( 357 new Comparator<SearchResult>() { 358 @Override 359 public int compare(SearchResult o1, SearchResult o2) { 360 float score1 = getRankingScoreByStableId(o1.stableId); 361 float score2 = getRankingScoreByStableId(o2.stableId); 362 if (score1 > score2) { 363 return -1; 364 } else if (score1 == score2) { 365 return 0; 366 } else { 367 return 1; 368 } 369 } 370 }); 371 dbResultsSortedByScores.addAll(databaseResults); 372 asyncRankingResults.addAll(dbResultsSortedByScores); 373 // Other results are not ranked by async ranking and appended at the end of the list. 374 asyncRankingResults.addAll(installedAppResults); 375 asyncRankingResults.addAll(accessibilityResults); 376 asyncRankingResults.addAll(inputDeviceResults); 377 return asyncRankingResults; 378 } 379 380 @VisibleForTesting getUnsortedLoadedResults(String loaderKey)381 Set<? extends SearchResult> getUnsortedLoadedResults(String loaderKey) { 382 return mResultsMap.containsKey(loaderKey) ? mResultsMap.get(loaderKey) : new HashSet<>(); 383 } 384 385 @VisibleForTesting getSortedLoadedResults(String loaderKey)386 List<? extends SearchResult> getSortedLoadedResults(String loaderKey) { 387 List<? extends SearchResult> sortedLoadedResults = 388 new ArrayList<>(getUnsortedLoadedResults(loaderKey)); 389 Collections.sort(sortedLoadedResults); 390 return sortedLoadedResults; 391 } 392 393 /** 394 * Looks up ranking score for stableId 395 * @param stableId String of stableId 396 * @return the ranking score corresponding to the given stableId. If there is no score 397 * available for this stableId, -Float.MAX_VALUE is returned. 398 */ 399 @VisibleForTesting getRankingScoreByStableId(int stableId)400 Float getRankingScoreByStableId(int stableId) { 401 for (Pair<String, Float> rankingScore : mSearchRankingScores) { 402 if (Integer.toString(stableId).compareTo(rankingScore.first) == 0) { 403 return rankingScore.second; 404 } 405 } 406 // If stableId not found in the list, we assign the minimum score so it will appear at 407 // the end of the list. 408 Log.w(TAG, "stableId " + stableId + " was not in the ranking scores."); 409 return -Float.MAX_VALUE; 410 } 411 412 @VisibleForTesting getHandler()413 Handler getHandler() { 414 if (mHandler == null) { 415 mHandler = new Handler(Looper.getMainLooper()) { 416 @Override 417 public void handleMessage(Message msg) { 418 if (msg.what == MSG_RANKING_TIMED_OUT) { 419 mSearchFeatureProvider.cancelPendingSearchQuery(mContext); 420 if (mAsyncRankingState == PENDING_RESULTS) { 421 mAsyncRankingState = TIMED_OUT; 422 if (canUpdateSearchResults()) { 423 updateSearchResults(); 424 } 425 } else { 426 Log.w(TAG, "Ranking scores timed out in invalid state: " + 427 mAsyncRankingState); 428 } 429 } 430 } 431 }; 432 } 433 return mHandler; 434 } 435 436 @VisibleForTesting postSearchResults(List<SearchResult> newSearchResults, boolean detectMoves)437 public void postSearchResults(List<SearchResult> newSearchResults, boolean detectMoves) { 438 final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( 439 new SearchResultDiffCallback(mSearchResults, newSearchResults), detectMoves); 440 mSearchResults.clear(); 441 mSearchResults.addAll(newSearchResults); 442 diffResult.dispatchUpdatesTo(this); 443 mFragment.onSearchResultsDisplayed(mSearchResults.size()); 444 mSearchResultsUpdated = true; 445 } 446 } 447