• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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