1 /* 2 * Copyright (C) 2009 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.globalsearch; 18 19 import android.content.ComponentName; 20 import android.database.Cursor; 21 import android.util.Log; 22 import android.app.SearchManager; 23 24 import java.lang.ref.SoftReference; 25 import java.util.ArrayList; 26 import java.util.HashMap; 27 import java.util.HashSet; 28 import java.util.List; 29 import java.util.LinkedHashMap; 30 import java.util.Collection; 31 import java.util.concurrent.Executor; 32 import java.util.concurrent.atomic.AtomicInteger; 33 34 /** 35 * A suggestion session lives from when the user starts typing into the search dialog until 36 * he/she is done (either clicked on a result, or dismissed the dialog). It caches results 37 * for the duration of a session, and aggregates stats about the session once it the session is 38 * closed. 39 * 40 * During the session, no {@link SuggestionSource} will be queried more than once for a given query. 41 * 42 * If a given source returns zero results for a query, that source will be ignored for supersets of 43 * that query for the rest of the session. Sources can opt out by setting their 44 * <code>queryAfterZeroResults</code> property to <code>true</code> in searchable.xml 45 * 46 * If there are no shortcuts or cached entries for a given query, we prefill with the results from 47 * the previous query for up to {@link Config#getPrefillMillis} ms until 48 * the first result comes back. 49 * This results in a smoother experience with less flickering of zero results. 50 * 51 * This class is thread safe, guarded by "this", to protect against the fact that {@link #query} 52 * and the callbacks via {@link com.android.globalsearch.SuggestionCursor.CursorListener} may be 53 * called by different threads (the filter thread of the ACTV, and the main thread respectively). 54 * Because {@link #query} is always called from the same thread, this synchronization does not 55 * impose any noticeable burden (and it is not necessary to attempt finer grained synchronization 56 * within the method). 57 */ 58 public class SuggestionSession { 59 private static final boolean DBG = false; 60 private static final boolean SPEW = false; 61 private static final String TAG = "GlobalSearch"; 62 63 private final Config mConfig; 64 private final SourceLookup mSourceLookup; 65 private final ArrayList<SuggestionSource> mPromotableSources; 66 private final ArrayList<SuggestionSource> mUnpromotableSources; 67 private ClickLogger mClickLogger; 68 private ShortcutRepository mShortcutRepo; 69 private final PerTagExecutor mQueryExecutor; 70 private final Executor mRefreshExecutor; 71 private final DelayedExecutor mDelayedExecutor; 72 private final SuggestionFactory mSuggestionFactory; 73 private SessionCallback mListener; 74 private int mNumPromotedSources; 75 76 // guarded by "this" 77 78 private final SessionCache mSessionCache; 79 80 // the cursor from the last character typed, if any 81 private SuggestionCursor mPreviousCursor = null; 82 83 // used to detect the closing of the session 84 private final AtomicInteger mOutstandingQueryCount = new AtomicInteger(0); 85 86 // we only allow shortcuts from sources in this set 87 private HashSet<ComponentName> mAllowShortcutsFrom; 88 89 /** 90 * Whether we cache the results for each query / source. This avoids querying a source twice 91 * for the same query, but uses more memory. 92 */ 93 static final boolean CACHE_SUGGESTION_RESULTS = false; 94 95 /** 96 * Interface for receiving notifications from session. 97 */ 98 interface SessionCallback { 99 100 /** 101 * Called when the session is over. 102 */ closeSession()103 void closeSession(); 104 } 105 106 /** 107 * @param config Configuration parameters 108 * @param sourceLookup The sources to query for results 109 * @param promotableSources The promotable sources, in the order that they should be queried. If the 110 * web source is enabled, it will always be first. 111 * @param unpromotableSources The unpromotable sources, in the order that they should be queried. 112 * @param queryExecutor Used to execute the asynchronous queries 113 * @param refreshExecutor Used to execute refresh tasks. 114 * @param delayedExecutor Used to post messages. 115 * @param suggestionFactory Used to create particular suggestions. 116 * @param cacheSuggestionResults Whether to cache the results of sources in hopes we can avoid 117 */ SuggestionSession( Config config, SourceLookup sourceLookup, ArrayList<SuggestionSource> promotableSources, ArrayList<SuggestionSource> unpromotableSources, PerTagExecutor queryExecutor, Executor refreshExecutor, DelayedExecutor delayedExecutor, SuggestionFactory suggestionFactory, boolean cacheSuggestionResults)118 public SuggestionSession( 119 Config config, 120 SourceLookup sourceLookup, 121 ArrayList<SuggestionSource> promotableSources, 122 ArrayList<SuggestionSource> unpromotableSources, 123 PerTagExecutor queryExecutor, 124 Executor refreshExecutor, 125 DelayedExecutor delayedExecutor, 126 SuggestionFactory suggestionFactory, 127 boolean cacheSuggestionResults) { 128 mConfig = config; 129 mSourceLookup = sourceLookup; 130 mPromotableSources = promotableSources; 131 mUnpromotableSources = unpromotableSources; 132 mQueryExecutor = queryExecutor; 133 mRefreshExecutor = refreshExecutor; 134 mDelayedExecutor = delayedExecutor; 135 mSuggestionFactory = suggestionFactory; 136 mSessionCache = new SessionCache(cacheSuggestionResults); 137 mNumPromotedSources = config.getNumPromotedSources(); 138 139 final int numPromotable = promotableSources.size(); 140 final int numUnpromotable = unpromotableSources.size(); 141 mAllowShortcutsFrom = new HashSet<ComponentName>(numPromotable + numUnpromotable); 142 mAllowShortcutsFrom.add(mSuggestionFactory.getSource()); 143 for (int i = 0; i < numPromotable; i++) { 144 mAllowShortcutsFrom.add(promotableSources.get(i).getComponentName()); 145 } 146 for (int i = 0; i < numUnpromotable; i++) { 147 mAllowShortcutsFrom.add(unpromotableSources.get(i).getComponentName()); 148 } 149 150 if (DBG) Log.d(TAG, "starting session"); 151 } 152 153 /** 154 * Sets a listener that will be notified of session events. 155 */ setListener(SessionCallback listener)156 public synchronized void setListener(SessionCallback listener) { 157 mListener = listener; 158 } 159 setClickLogger(ClickLogger clickLogger)160 public synchronized void setClickLogger(ClickLogger clickLogger) { 161 mClickLogger = clickLogger; 162 } 163 setShortcutRepo(ShortcutRepository shortcutRepo)164 public synchronized void setShortcutRepo(ShortcutRepository shortcutRepo) { 165 mShortcutRepo = shortcutRepo; 166 } 167 168 /** 169 * @param numPromotedSources The number of sources to query first for the promoted list. 170 */ setNumPromotedSources(int numPromotedSources)171 public synchronized void setNumPromotedSources(int numPromotedSources) { 172 mNumPromotedSources = numPromotedSources; 173 } 174 175 /** 176 * Queries the current session for a resulting cursor. The cursor will be backed by shortcut 177 * and cached data from this session and then be notified of change as other results come in. 178 * 179 * @param query The query. 180 * @return A cursor. 181 */ query(final String query)182 public synchronized Cursor query(final String query) { 183 mOutstandingQueryCount.incrementAndGet(); 184 185 final SuggestionCursor cursor = new SuggestionCursor(mDelayedExecutor, query); 186 187 fireStuffOff(cursor, query); 188 189 // if the cursor we are about to return is empty (no cache, no shortcuts), 190 // prefill it with the previous results until we hear back from a source 191 if (mPreviousCursor != null 192 && query.length() > 1 // don't prefil when going from empty to first char 193 && cursor.getCount() == 0 194 && mPreviousCursor.getCount() > 0) { 195 cursor.prefill(mPreviousCursor); 196 197 // limit the amount of time we show prefilled results 198 mDelayedExecutor.postDelayed(new Runnable() { 199 public void run() { 200 cursor.onNewResults(); 201 } 202 }, mConfig.getPrefillMillis()); 203 } 204 mPreviousCursor = cursor; 205 return cursor; 206 } 207 208 /** 209 * Finishes the work necessary to report complete results back to the cursor. This includes 210 * getting the shortcuts, refreshing them, determining which source should be queried, sending 211 * off the query to each of them, and setting up the callback from the cursor. 212 * 213 * @param cursor The cursor the results will be reported to. 214 * @param query The query. 215 */ fireStuffOff(final SuggestionCursor cursor, final String query)216 private void fireStuffOff(final SuggestionCursor cursor, final String query) { 217 // get shortcuts 218 final ArrayList<SuggestionData> shortcuts = getShortcuts(query); 219 220 // filter out sources that aren't relevant to this query 221 final ArrayList<SuggestionSource> promotableSourcesToQuery = 222 filterSourcesForQuery(query, mPromotableSources); 223 final ArrayList<SuggestionSource> unpromotableSourcesToQuery = 224 filterSourcesForQuery(query, mUnpromotableSources); 225 final ArrayList<SuggestionSource> sourcesToQuery 226 = new ArrayList<SuggestionSource>( 227 promotableSourcesToQuery.size() + unpromotableSourcesToQuery.size()); 228 sourcesToQuery.addAll(promotableSourcesToQuery); 229 sourcesToQuery.addAll(unpromotableSourcesToQuery); 230 231 if (DBG) { 232 Log.d(TAG, promotableSourcesToQuery.size() + " promotable sources and " 233 + promotableSourcesToQuery.size() + " unpromotable sources will be queried."); 234 } 235 236 // get the shortcuts to refresh 237 final ArrayList<SuggestionData> shortcutsToRefresh = new ArrayList<SuggestionData>(); 238 final int numShortcuts = shortcuts.size(); 239 for (int i = 0; i < numShortcuts; i++) { 240 SuggestionData shortcut = shortcuts.get(i); 241 242 final String shortcutId = shortcut.getShortcutId(); 243 if (shortcutId == null) continue; 244 245 if (mSessionCache.hasShortcutBeenRefreshed(shortcut.getSource(), shortcutId)) { 246 // if we've already refreshed the shortcut, don't do it again. if it shows a 247 // spinner while refreshing, it will come out of the repo with a spinner for icon2. 248 // we need to remove this or replace it with what was refreshed as applicable. 249 if (shortcut.isSpinnerWhileRefreshing()) { 250 shortcuts.set( 251 i, 252 shortcut.buildUpon().icon2( 253 mSessionCache.getRefreshedShortcutIcon2( 254 shortcut.getSource(), shortcutId)).build()); 255 } 256 continue; 257 } 258 shortcutsToRefresh.add(shortcut); 259 } 260 261 // make the suggestion backer 262 final HashSet<ComponentName> promoted = pickPromotedSources(promotableSourcesToQuery); 263 264 // cached source results 265 final QueryCacheResults queryCacheResults = mSessionCache.getSourceResults(query); 266 267 final SuggestionSource webSearchSource = mSourceLookup.getSelectedWebSearchSource(); 268 final SourceSuggestionBacker backer = new SourceSuggestionBacker( 269 query, 270 shortcuts, 271 sourcesToQuery, 272 promoted, 273 webSearchSource, 274 queryCacheResults.getResults(), 275 mSuggestionFactory.createGoToWebsiteSuggestion(query), 276 mSuggestionFactory.createSearchTheWebSuggestion(query), 277 mConfig.getMaxResultsToDisplay(), 278 mConfig.getPromotedSourceDeadlineMillis(), 279 mSuggestionFactory, 280 mSuggestionFactory); 281 282 if (DBG) { 283 Log.d(TAG, "starting off with " + queryCacheResults.getResults().size() + " cached " 284 + "sources"); 285 Log.d(TAG, "identified " + promoted.size() + " promoted sources to query"); 286 Log.d(TAG, "identified " + shortcutsToRefresh.size() 287 + " shortcuts out of " + numShortcuts + " total shortcuts to refresh"); 288 } 289 290 // fire off queries / refreshers 291 final AsyncMux asyncMux = new AsyncMux( 292 mConfig, 293 mQueryExecutor, 294 mRefreshExecutor, 295 mDelayedExecutor, 296 mSessionCache, 297 query, 298 shortcutsToRefresh, 299 removeCached(sourcesToQuery, queryCacheResults), 300 promoted, 301 backer, 302 mShortcutRepo); 303 304 cursor.attachBacker(asyncMux); 305 asyncMux.setListener(cursor); 306 307 cursor.setListener(new SessionCursorListener(asyncMux)); 308 309 asyncMux.sendOffShortcutRefreshers(mSourceLookup); 310 asyncMux.sendOffPromotedSourceQueries(); 311 312 // refresh the backer after the deadline to force showing of "more results" 313 // even if all of the promoted sources haven't responded yet. 314 mDelayedExecutor.postDelayed(new Runnable() { 315 public void run() { 316 cursor.onNewResults(); 317 } 318 }, mConfig.getPromotedSourceDeadlineMillis()); 319 } 320 pickPromotedSources(ArrayList<SuggestionSource> sources)321 private HashSet<ComponentName> pickPromotedSources(ArrayList<SuggestionSource> sources) { 322 HashSet<ComponentName> promoted = new HashSet<ComponentName>(sources.size()); 323 for (int i = 0; i < mNumPromotedSources && i < sources.size(); i++) { 324 promoted.add(sources.get(i).getComponentName()); 325 } 326 return promoted; 327 } 328 329 private class SessionCursorListener implements SuggestionCursor.CursorListener { 330 private AsyncMux mAsyncMux; SessionCursorListener(AsyncMux asyncMux)331 public SessionCursorListener(AsyncMux asyncMux) { 332 mAsyncMux = asyncMux; 333 } onClose()334 public void onClose() { 335 if (DBG) Log.d(TAG, "onClose(\"" + mAsyncMux.getQuery() + "\")"); 336 337 mAsyncMux.cancel(); 338 // when the cursor closes and there aren't any outstanding requests, it means 339 // the user has moved on (either clicked on something, dismissed the dialog, or 340 // pivoted into app specific search) 341 int refCount = mOutstandingQueryCount.decrementAndGet(); 342 if (DBG) Log.d(TAG, "Session reference count: " + refCount); 343 if (refCount == 0) { 344 close(); 345 } 346 } 347 onItemClicked(int pos, List<SuggestionData> viewedSuggestions, int actionKey, String actionMsg)348 public void onItemClicked(int pos, List<SuggestionData> viewedSuggestions, 349 int actionKey, String actionMsg) { 350 if (DBG) Log.d(TAG, "onItemClicked(" + pos + ")"); 351 SuggestionData clicked = viewedSuggestions.get(pos); 352 String query = mAsyncMux.getQuery(); 353 354 // Report click to click logger 355 if (mClickLogger != null) { 356 mClickLogger.logClick(query, pos, viewedSuggestions, actionKey, actionMsg); 357 } 358 359 SuggestionData clickedSuggestion = null; 360 // Only record clicks on suggestions that are shortcuttable or from external sources 361 if (isShortcuttable(clicked) || isSourceSuggestion(clicked)) { 362 clickedSuggestion = clicked; 363 } 364 365 // find impressions to report 366 HashSet<ComponentName> sourceImpressions = getSourceImpressions(viewedSuggestions); 367 368 // Report impressions and click to shortcut repository 369 reportStats(new SessionStats(query, clickedSuggestion, sourceImpressions)); 370 } 371 getSourceImpressions( List<SuggestionData> viewedSuggestions)372 private HashSet<ComponentName> getSourceImpressions( 373 List<SuggestionData> viewedSuggestions) { 374 final int numViewed = viewedSuggestions.size(); 375 HashSet<ComponentName> sourceImpressions = new HashSet<ComponentName>(); 376 for (int i = 0; i < numViewed; i++) { 377 final SuggestionData viewed = viewedSuggestions.get(i); 378 // only add it if it is from a source we know of (e.g, not a built in one 379 // used for special suggestions like "more results"). 380 if (isSourceSuggestion(viewed)) { 381 sourceImpressions.add(viewed.getSource()); 382 } else if (isCorpusSelector(viewed)) { 383 // a corpus result under "more results"; unpack the component 384 final ComponentName corpusName = 385 ComponentName.unflattenFromString(viewed.getIntentData()); 386 if (corpusName != null && mAsyncMux.hasSourceStarted(corpusName)) { 387 // we only count an impression if the source has at least begun 388 // retrieving its results. 389 sourceImpressions.add(corpusName); 390 } 391 } 392 } 393 return sourceImpressions; 394 } 395 isShortcuttable(SuggestionData suggestion)396 private boolean isShortcuttable(SuggestionData suggestion) { 397 return !SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT.equals(suggestion.getShortcutId()); 398 } 399 400 /** 401 * Checks whether a suggestion comes from a source we know of (e.g, not a built in one 402 * used for special suggestions like "more results"). 403 */ isSourceSuggestion(SuggestionData suggestion)404 private boolean isSourceSuggestion(SuggestionData suggestion) { 405 return mSourceLookup.getSourceByComponentName(suggestion.getSource()) != null; 406 } 407 isCorpusSelector(SuggestionData suggestion)408 private boolean isCorpusSelector(SuggestionData suggestion) { 409 return SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.equals( 410 suggestion.getIntentAction()); 411 } 412 onMoreVisible()413 public void onMoreVisible() { 414 if (DBG) Log.d(TAG, "onMoreVisible"); 415 mAsyncMux.sendOffAdditionalSourcesQueries(); 416 } 417 onSearch(String query, List<SuggestionData> viewedSuggestions)418 public void onSearch(String query, List<SuggestionData> viewedSuggestions) { 419 // find impressions to report 420 HashSet<ComponentName> sourceImpressions = getSourceImpressions(viewedSuggestions); 421 SuggestionData searchSuggestion = mSuggestionFactory.createWebSearchShortcut(query); 422 reportStats(new SessionStats(query, searchSuggestion, sourceImpressions)); 423 } 424 } 425 getShortcuts(String query)426 private ArrayList<SuggestionData> getShortcuts(String query) { 427 if (mShortcutRepo == null) return new ArrayList<SuggestionData>(); 428 return filterOnlyEnabled(mShortcutRepo.getShortcutsForQuery(query)); 429 } 430 reportStats(SessionStats stats)431 void reportStats(SessionStats stats) { 432 if (mShortcutRepo != null) mShortcutRepo.reportStats(stats); 433 } 434 close()435 synchronized void close() { 436 if (DBG) Log.d(TAG, "close()"); 437 if (mListener != null) mListener.closeSession(); 438 } 439 440 /** 441 * Filter the list of shortcuts to only include those come from enabled sources. 442 * 443 * @param shortcutsForQuery The shortcuts. 444 * @return A list including only shortcuts from sources that are enabled. 445 */ filterOnlyEnabled( ArrayList<SuggestionData> shortcutsForQuery)446 private ArrayList<SuggestionData> filterOnlyEnabled( 447 ArrayList<SuggestionData> shortcutsForQuery) { 448 final int numShortcuts = shortcutsForQuery.size(); 449 if (numShortcuts == 0) return shortcutsForQuery; 450 451 final ArrayList<SuggestionData> result = new ArrayList<SuggestionData>( 452 shortcutsForQuery.size()); 453 for (int i = 0; i < numShortcuts; i++) { 454 final SuggestionData shortcut = shortcutsForQuery.get(i); 455 if (mAllowShortcutsFrom.contains(shortcut.getSource())) { 456 result.add(shortcut); 457 } 458 } 459 return result; 460 } 461 462 /** 463 * @param sources The sources 464 * @param queryCacheResults The cached results for the current query 465 * @return A list of sources not including any of the cached results. 466 */ removeCached( ArrayList<SuggestionSource> sources, QueryCacheResults queryCacheResults)467 private ArrayList<SuggestionSource> removeCached( 468 ArrayList<SuggestionSource> sources, QueryCacheResults queryCacheResults) { 469 final int numSources = sources.size(); 470 final ArrayList<SuggestionSource> unCached = new ArrayList<SuggestionSource>(numSources); 471 472 for (int i = 0; i < numSources; i++) { 473 final SuggestionSource source = sources.get(i); 474 if (queryCacheResults.getResult(source.getComponentName()) == null) { 475 unCached.add(source); 476 } 477 } 478 return unCached; 479 } 480 481 /** 482 * Filter the sources to query based on properties of each source related to the query. 483 * 484 * @param query The query. 485 * @param enabledSources The full list of sources. 486 * @return A list of sources that should be queried. 487 */ filterSourcesForQuery( String query, ArrayList<SuggestionSource> enabledSources)488 private ArrayList<SuggestionSource> filterSourcesForQuery( 489 String query, ArrayList<SuggestionSource> enabledSources) { 490 final int queryLength = query.length(); 491 final int cutoff = Math.max(1, queryLength); 492 final ArrayList<SuggestionSource> sourcesToQuery = new ArrayList<SuggestionSource>(); 493 494 if (queryLength == 0) return sourcesToQuery; 495 496 if (DBG && SPEW) Log.d(TAG, "filtering enabled sources to those we want to query..."); 497 for (SuggestionSource enabledSource : enabledSources) { 498 499 // query too short 500 if (enabledSource.getQueryThreshold() > cutoff) { 501 if (DBG && SPEW) { 502 Log.d(TAG, "skipping " + enabledSource.getLabel() + " (query thresh)"); 503 } 504 continue; 505 } 506 507 final ComponentName sourceName = enabledSource.getComponentName(); 508 509 // source returned zero results for a prefix of query 510 if (!enabledSource.queryAfterZeroResults() 511 && mSessionCache.hasReportedZeroResultsForPrefix( 512 query, sourceName)) { 513 if (DBG && SPEW) { 514 Log.d(TAG, "skipping " + enabledSource.getLabel() 515 + " (zero results for prefix)"); 516 } 517 continue; 518 } 519 520 if (DBG && SPEW) Log.d(TAG, "adding " + enabledSource.getLabel()); 521 sourcesToQuery.add(enabledSource); 522 } 523 return sourcesToQuery; 524 } 525 getNow()526 long getNow() { 527 return System.currentTimeMillis(); 528 } 529 530 /** 531 * Caches results and information to avoid doing unnecessary work within the session. Helps 532 * the session to make the following optimizations: 533 * - don't query same source more than once for a given query (subject to memory constraints) 534 * - don't validate the same shortcut more than once 535 * - don't query a source again if it returned zero results before for a prefix of a given query 536 * 537 * To avoid hogging memory the list of suggestions returned from sources are referenced from 538 * soft references. 539 */ 540 static class SessionCache { 541 542 static final QueryCacheResults EMPTY = new QueryCacheResults(); 543 static private final String NO_ICON = "NO_ICON"; 544 545 private final HashMap<String, HashSet<ComponentName>> mZeroResultSources 546 = new HashMap<String, HashSet<ComponentName>>(); 547 548 private final HashMap<String, SoftReference<QueryCacheResults>> mResultsCache; 549 private final HashMap<String, String> mRefreshedShortcuts = new HashMap<String, String>(); 550 SessionCache(boolean cacheQueryResults)551 SessionCache(boolean cacheQueryResults) { 552 mResultsCache = cacheQueryResults ? 553 new HashMap<String, SoftReference<QueryCacheResults>>() : 554 null; 555 } 556 557 /** 558 * @param query The query 559 * @param source Identifies the source 560 * @return Whether the given source has returned zero results for any prefixes of the 561 * given query. 562 */ hasReportedZeroResultsForPrefix( String query, ComponentName source)563 synchronized boolean hasReportedZeroResultsForPrefix( 564 String query, ComponentName source) { 565 final int queryLength = query.length(); 566 for (int i = 1; i < queryLength; i++) { 567 final String subQuery = query.substring(0, queryLength - i); 568 final HashSet<ComponentName> zeros = mZeroResultSources.get(subQuery); 569 if (zeros != null && zeros.contains(source)) { 570 return true; 571 } 572 } 573 return false; 574 } 575 576 /** 577 * Reports that a source has refreshed a shortcut 578 */ reportRefreshedShortcut( ComponentName source, String shortcutId, SuggestionData shortcut)579 synchronized void reportRefreshedShortcut( 580 ComponentName source, String shortcutId, SuggestionData shortcut) { 581 final String icon2 = (shortcut == null || !shortcut.isSpinnerWhileRefreshing()) ? 582 NO_ICON : 583 (shortcut.getIcon2() == null) ? NO_ICON : shortcut.getIcon2(); 584 mRefreshedShortcuts.put(makeShortcutKey(source, shortcutId), icon2); 585 } 586 587 /** 588 * @param source Identifies the source 589 * @param shortcutId The id of the shortcut 590 * @return Whether the shortcut id has been validated already 591 */ hasShortcutBeenRefreshed( ComponentName source, String shortcutId)592 synchronized boolean hasShortcutBeenRefreshed( 593 ComponentName source, String shortcutId) { 594 return mRefreshedShortcuts.containsKey(makeShortcutKey(source, shortcutId)); 595 } 596 597 /** 598 * @return The icon2 that was reported by the refreshed source, or null if there was no 599 * icon2 in the refreshed shortcut. Also returns null if the shortcut was never 600 * refreshed, or if the shortcut is not 601 * {@link SuggestionData#isSpinnerWhileRefreshing()}. 602 */ getRefreshedShortcutIcon2(ComponentName source, String shortcutId)603 synchronized String getRefreshedShortcutIcon2(ComponentName source, String shortcutId) { 604 final String icon2 = mRefreshedShortcuts.get(makeShortcutKey(source, shortcutId)); 605 return (icon2 == null || icon2 == NO_ICON) ? null : icon2; 606 } 607 makeShortcutKey(ComponentName name, String shortcutId)608 private static String makeShortcutKey(ComponentName name, String shortcutId) { 609 final String nameStr = name.toShortString(); 610 return new StringBuilder(nameStr.length() + shortcutId.length() + 1) 611 .append(nameStr).append('_').append(shortcutId).toString(); 612 } 613 614 /** 615 * @param query The query 616 * @return The results for any sources that have reported results. 617 */ getSourceResults(String query)618 synchronized QueryCacheResults getSourceResults(String query) { 619 final QueryCacheResults queryCacheResults = getCachedResult(query); 620 return queryCacheResults == null ? EMPTY : queryCacheResults; 621 } 622 623 624 /** 625 * Reports that a source has provided results for a particular query. 626 */ reportSourceResult(String query, SuggestionResult sourceResult)627 synchronized void reportSourceResult(String query, SuggestionResult sourceResult) { 628 629 // caching of query results 630 if (mResultsCache != null) { 631 QueryCacheResults queryCacheResults = getCachedResult(query); 632 if (queryCacheResults == null) { 633 queryCacheResults = new QueryCacheResults(); 634 mResultsCache.put( 635 query, new SoftReference<QueryCacheResults>(queryCacheResults)); 636 } 637 queryCacheResults.addResult(sourceResult); 638 } 639 640 // book keeping about sources that have returned zero results 641 if (!sourceResult.getSource().queryAfterZeroResults() 642 && sourceResult.getSuggestions().isEmpty()) { 643 HashSet<ComponentName> zeros = mZeroResultSources.get(query); 644 if (zeros == null) { 645 zeros = new HashSet<ComponentName>(); 646 mZeroResultSources.put(query, zeros); 647 } 648 zeros.add(sourceResult.getSource().getComponentName()); 649 } 650 } 651 getCachedResult(String query)652 private QueryCacheResults getCachedResult(String query) { 653 if (mResultsCache == null) return null; 654 655 final SoftReference<QueryCacheResults> ref = mResultsCache.get(query); 656 if (ref == null) return null; 657 658 if (ref.get() == null) { 659 if (DBG) Log.d(TAG, "soft ref to results for '" + query + "' GC'd"); 660 } 661 return ref.get(); 662 } 663 } 664 665 /** 666 * Holds the results reported back by the sources for a particular query. 667 * 668 * Preserves order of when they were reported back, provides efficient lookup for a given 669 * source 670 */ 671 static class QueryCacheResults { 672 673 private final LinkedHashMap<ComponentName, SuggestionResult> mSourceResults 674 = new LinkedHashMap<ComponentName, SuggestionResult>(); 675 addResult(SuggestionResult result)676 public void addResult(SuggestionResult result) { 677 mSourceResults.put(result.getSource().getComponentName(), result); 678 } 679 getResults()680 public Collection<SuggestionResult> getResults() { 681 return mSourceResults.values(); 682 } 683 getResult(ComponentName source)684 public SuggestionResult getResult(ComponentName source) { 685 return mSourceResults.get(source); 686 } 687 } 688 689 /** 690 * Asynchronously queries sources to get their results for a query and to validate shorcuts. 691 * 692 * Results are passed through to a wrapped {@link SuggestionBacker} after passing along stats 693 * to the session cache. 694 */ 695 static class AsyncMux extends SuggestionBacker { 696 697 private final Config mConfig; 698 private final PerTagExecutor mQueryExecutor; 699 private final Executor mRefreshExecutor; 700 private final DelayedExecutor mDelayedExecutor; 701 private final SessionCache mSessionCache; 702 private final String mQuery; 703 private final ArrayList<SuggestionData> mShortcutsToValidate; 704 private final ArrayList<SuggestionSource> mSourcesToQuery; 705 private final HashSet<ComponentName> mPromotedSources; 706 private final SourceSuggestionBacker mBackerToReportTo; 707 private final ShortcutRepository mRepo; 708 709 private QueryMultiplexer mPromotedSourcesQueryMux; 710 private QueryMultiplexer mAdditionalSourcesQueryMux; 711 private ShortcutRefresher mShortcutRefresher; 712 713 private volatile boolean mCanceled = false; 714 715 /** 716 * @param config Configuration parameters. 717 * @param queryExecutor required by the query multiplexers. 718 * @param refreshExecutor required by the refresh multiplexers. 719 * @param delayedExecutor required by the query multiplexers. 720 * @param sessionCache results are repoted to the cache as they come in 721 * @param query the query the tasks pertain to 722 * @param shortcutsToValidate the shortcuts that need to be validated 723 * @param sourcesToQuery the sources that need to be queried 724 * @param promotedSources those sources that are promoted 725 * @param backerToReportTo the backer the results should be passed to 726 * @param repo The shortcut repository needed to create the shortcut refresher. 727 */ AsyncMux( Config config, PerTagExecutor queryExecutor, Executor refreshExecutor, DelayedExecutor delayedExecutor, SessionCache sessionCache, String query, ArrayList<SuggestionData> shortcutsToValidate, ArrayList<SuggestionSource> sourcesToQuery, HashSet<ComponentName> promotedSources, SourceSuggestionBacker backerToReportTo, ShortcutRepository repo)728 AsyncMux( 729 Config config, 730 PerTagExecutor queryExecutor, 731 Executor refreshExecutor, 732 DelayedExecutor delayedExecutor, 733 SessionCache sessionCache, 734 String query, 735 ArrayList<SuggestionData> shortcutsToValidate, 736 ArrayList<SuggestionSource> sourcesToQuery, 737 HashSet<ComponentName> promotedSources, 738 SourceSuggestionBacker backerToReportTo, 739 ShortcutRepository repo) { 740 mConfig = config; 741 mQueryExecutor = queryExecutor; 742 mRefreshExecutor = refreshExecutor; 743 mDelayedExecutor = delayedExecutor; 744 mSessionCache = sessionCache; 745 mQuery = query; 746 mShortcutsToValidate = shortcutsToValidate; 747 mSourcesToQuery = sourcesToQuery; 748 mPromotedSources = promotedSources; 749 mBackerToReportTo = backerToReportTo; 750 mRepo = repo; 751 } 752 getQuery()753 public String getQuery() { 754 return mQuery; 755 } 756 757 @Override snapshotSuggestions(ArrayList<SuggestionData> dest, boolean expandAdditional)758 public void snapshotSuggestions(ArrayList<SuggestionData> dest, boolean expandAdditional) { 759 mBackerToReportTo.snapshotSuggestions(dest, expandAdditional); 760 } 761 762 @Override isResultsPending()763 public boolean isResultsPending() { 764 return mBackerToReportTo.isResultsPending(); 765 } 766 767 @Override isShowingMore()768 public boolean isShowingMore() { 769 return mBackerToReportTo.isShowingMore(); 770 } 771 772 @Override getMoreResultPosition()773 public int getMoreResultPosition() { 774 return mBackerToReportTo.getMoreResultPosition(); 775 } 776 777 @Override reportSourceStarted(ComponentName source)778 public boolean reportSourceStarted(ComponentName source) { 779 return mBackerToReportTo.reportSourceStarted(source); 780 } 781 782 @Override hasSourceStarted(ComponentName source)783 public boolean hasSourceStarted(ComponentName source) { 784 return mBackerToReportTo.hasSourceStarted(source); 785 } 786 787 @Override addSourceResults(SuggestionResult suggestionResult)788 protected boolean addSourceResults(SuggestionResult suggestionResult) { 789 if (suggestionResult.getResultCode() == SuggestionResult.RESULT_OK) { 790 mSessionCache.reportSourceResult(mQuery, suggestionResult); 791 } 792 return mBackerToReportTo.addSourceResults(suggestionResult); 793 } 794 795 @Override refreshShortcut( ComponentName source, String shortcutId, SuggestionData shortcut)796 protected boolean refreshShortcut( 797 ComponentName source, String shortcutId, SuggestionData shortcut) { 798 mSessionCache.reportRefreshedShortcut(source, shortcutId, shortcut); 799 return mBackerToReportTo.refreshShortcut(source, shortcutId, shortcut); 800 } 801 sendOffShortcutRefreshers(SourceLookup sourceLookup)802 void sendOffShortcutRefreshers(SourceLookup sourceLookup) { 803 if (mCanceled) return; 804 if (mShortcutRefresher != null) { 805 throw new IllegalStateException("Already refreshed once"); 806 } 807 mShortcutRefresher = new ShortcutRefresher( 808 mRefreshExecutor, sourceLookup, mShortcutsToValidate, 809 mConfig.getMaxResultsToDisplay(), this, mRepo); 810 if (DBG) Log.d(TAG, "sending shortcut refresher tasks for " + 811 mShortcutsToValidate.size() + " shortcuts."); 812 mShortcutRefresher.refresh(); 813 } 814 sendOffPromotedSourceQueries()815 void sendOffPromotedSourceQueries() { 816 if (mCanceled) return; 817 if (mPromotedSourcesQueryMux != null) { 818 throw new IllegalStateException("Already queried once"); 819 } 820 821 ArrayList<SuggestionSource> promotedSources = 822 new ArrayList<SuggestionSource>(mPromotedSources.size()); 823 824 for (SuggestionSource source : mSourcesToQuery) { 825 if (mPromotedSources.contains(source.getComponentName())) { 826 promotedSources.add(source); 827 } 828 } 829 final int maxResultsPerSource = mConfig.getMaxResultsPerSource(); 830 mPromotedSourcesQueryMux = new QueryMultiplexer( 831 mQuery, promotedSources, 832 maxResultsPerSource, 833 mConfig.getWebResultsOverrideLimit(), 834 maxResultsPerSource, 835 this, mQueryExecutor, mDelayedExecutor, 836 mConfig.getSourceTimeoutMillis()); 837 if (DBG) Log.d(TAG, "sending '" + mQuery + "' off to " + promotedSources.size() + 838 " promoted sources"); 839 mBackerToReportTo.reportPromotedQueryStartTime(); 840 mPromotedSourcesQueryMux.sendQuery(); 841 } 842 sendOffAdditionalSourcesQueries()843 void sendOffAdditionalSourcesQueries() { 844 if (mCanceled) return; 845 if (mAdditionalSourcesQueryMux != null) { 846 throw new IllegalStateException("Already queried once"); 847 } 848 849 final int numAdditional = mSourcesToQuery.size() - mPromotedSources.size(); 850 851 if (numAdditional <= 0) { 852 return; 853 } 854 855 ArrayList<SuggestionSource> additional = new ArrayList<SuggestionSource>(numAdditional); 856 for (SuggestionSource source : mSourcesToQuery) { 857 if (!mPromotedSources.contains(source.getComponentName())) { 858 additional.add(source); 859 } 860 } 861 862 mAdditionalSourcesQueryMux = new QueryMultiplexer( 863 mQuery, additional, 864 mConfig.getMaxResultsToDisplay(), 865 mConfig.getWebResultsOverrideLimit(), 866 mConfig.getMaxResultsPerSource(), 867 this, mQueryExecutor, mDelayedExecutor, 868 mConfig.getSourceTimeoutMillis()); 869 if (DBG) Log.d(TAG, "sending queries off to " + additional.size() + " promoted " + 870 "sources"); 871 mAdditionalSourcesQueryMux.sendQuery(); 872 } 873 cancel()874 void cancel() { 875 mCanceled = true; 876 877 if (mShortcutRefresher != null) { 878 mShortcutRefresher.cancel(); 879 } 880 if (mPromotedSourcesQueryMux != null) { 881 mPromotedSourcesQueryMux.cancel(); 882 } 883 if (mAdditionalSourcesQueryMux != null) { 884 mAdditionalSourcesQueryMux.cancel(); 885 } 886 } 887 } 888 } 889