• 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 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