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