• 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 
18 package com.android.globalsearch;
19 
20 import com.google.android.collect.Lists;
21 
22 import android.app.SearchManager;
23 import android.app.SearchManager.DialogCursorProtocol;
24 import android.content.ComponentName;
25 import android.content.Intent;
26 import android.database.Cursor;
27 import android.os.Bundle;
28 import android.test.MoreAsserts;
29 import android.test.suitebuilder.annotation.SmallTest;
30 
31 import java.util.ArrayList;
32 import java.util.Iterator;
33 import java.util.LinkedHashMap;
34 import java.util.LinkedList;
35 import java.util.List;
36 import java.util.concurrent.Executor;
37 import java.util.concurrent.FutureTask;
38 
39 import junit.framework.TestCase;
40 
41 /**
42  * Tests for {@link SuggestionSession}, its interaction with {@link SuggestionCursor}, and how and
43  * when the session fires queries off to the suggestion sources.
44  */
45 @SmallTest
46 public class SuggestionSessionTest extends TestCase implements SuggestionFactory {
47 
48     private TestSuggestionSession mSession;
49     private QueryEngine mEngine;
50     private ComponentName mComponentA;
51     private SuggestionSource mSourceA;
52     private ComponentName mWebComponent;
53     private SuggestionSource mWebSource;
54     private SuggestionData mWebSuggestion;
55     private SuggestionData mSearchTheWebSuggestion;
56     private SuggestionData mSuggestionFromA;
57 
58     @Override
setUp()59     protected void setUp() throws Exception {
60 
61         mWebComponent = new ComponentName("com.android.web", "com.android.web.GOOG");
62         mWebSuggestion = makeSimple(mWebComponent, "a web a");
63         mSearchTheWebSuggestion = createSearchTheWebSuggestion("a");
64         mWebSource = new TestSuggestionSource.Builder()
65                 .setComponent(mWebComponent)
66                 .setLabel("web")
67                 .addCannedResponse("a", mWebSuggestion)
68                 .addCannedResponse("b", mWebSuggestion)
69                 .create();
70 
71         mComponentA = new ComponentName("com.android.test", "com.android.test.A");
72         mSuggestionFromA = makeSimple(mComponentA, "a 1");
73         mSourceA = new TestSuggestionSource.Builder()
74                 .setComponent(mComponentA)
75                 .setLabel("A")
76                 .addCannedResponse("a", mSuggestionFromA)
77                 .addCannedResponse("b", mSuggestionFromA)
78                 .create();
79 
80         ArrayList<SuggestionSource> promotableSources = Lists.newArrayList(mWebSource, mSourceA);
81         ArrayList<SuggestionSource> unpromotableSources = Lists.newArrayList();
82         mSession = initSession(promotableSources, unpromotableSources, mWebSource, 4);
83     }
84 
initSession( ArrayList<SuggestionSource> promotableSources, ArrayList<SuggestionSource> unpromotableSources, SuggestionSource webSource, int numPromotedSources)85     private TestSuggestionSession initSession(
86             ArrayList<SuggestionSource> promotableSources,
87             ArrayList<SuggestionSource> unpromotableSources,
88             SuggestionSource webSource, int numPromotedSources) {
89         ArrayList<SuggestionSource> allSources = new ArrayList<SuggestionSource>();
90         allSources.addAll(promotableSources);
91         allSources.addAll(unpromotableSources);
92         final SimpleSourceLookup sourceLookup = new SimpleSourceLookup(allSources, webSource);
93         Config config = Config.getDefaultConfig();
94         mEngine = new QueryEngine();
95         return new TestSuggestionSession(
96                 config,
97                 sourceLookup, promotableSources, unpromotableSources,
98                 this, mEngine, numPromotedSources);
99     }
100 
makeSimple(ComponentName component, String title)101     SuggestionData makeSimple(ComponentName component, String title) {
102         return new SuggestionData.Builder(component)
103                 .title(title)
104                 .intentAction("view")
105                 .intentData(title)
106                 .build();
107     }
108 
109 // --------------------- Interface SuggestionFactory ---------------------
110 
111     private static ComponentName BUILT_IN = new ComponentName("com.builtin", "class");
112     private static SuggestionData MORE =
113             new SuggestionData.Builder(BUILT_IN)
114             .title("more")
115             .shortcutId(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT)
116             .build();
117 
getSource()118     public ComponentName getSource() {
119         return BUILT_IN;
120     }
121 
createSearchTheWebSuggestion(String query)122     public SuggestionData createSearchTheWebSuggestion(String query) {
123         return new SuggestionData.Builder(BUILT_IN)
124                 .title("search the web for " + query)
125                 .intentAction(Intent.ACTION_WEB_SEARCH)
126                 .build();
127     }
128 
createWebSearchShortcut(String query)129     public SuggestionData createWebSearchShortcut(String query) {
130         return new SuggestionData.Builder(BUILT_IN)
131                 .title("web search shortcut for " + query)
132                 .intentAction(Intent.ACTION_WEB_SEARCH)
133                 .build();
134     }
135 
createGoToWebsiteSuggestion(String query)136     public SuggestionData createGoToWebsiteSuggestion(String query) { return null; }
137 
getMoreEntry( boolean expanded, List<SourceSuggestionBacker.SourceStat> sourceStats)138     public SuggestionData getMoreEntry(
139             boolean expanded, List<SourceSuggestionBacker.SourceStat> sourceStats) {
140         return MORE;
141     }
142 
getCorpusEntry( String query, SourceSuggestionBacker.SourceStat sourceStat)143     public SuggestionData getCorpusEntry(
144             String query, SourceSuggestionBacker.SourceStat sourceStat) {
145         final ComponentName name = sourceStat.getName();
146         return makeCorpusEntry(name);
147     }
148 
makeCorpusEntry(ComponentName name)149     private SuggestionData makeCorpusEntry(ComponentName name) {
150         return new SuggestionData.Builder(BUILT_IN)
151                 .intentAction(SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE)
152                 .intentData(name.flattenToShortString())
153                 .title("corpus " + name).build();
154     }
155 
156 // --------------------- Tests ---------------------
157 
158 
testBasicQuery()159     public void testBasicQuery() {
160         final Cursor cursor = mSession.query("a");
161         {
162             final Snapshot snapshot = getSnapshot(cursor);
163             assertTrue("isPending.", snapshot.isPending);
164             assertEquals("displayNotify", NONE, snapshot.displayNotify);
165             MoreAsserts.assertEmpty("suggestions", snapshot.suggestionTitles);
166 
167             MoreAsserts.assertContentsInOrder("sources in progress",
168                     mEngine.getPendingSources(),
169                     mWebComponent, mComponentA);
170         }
171 
172         mEngine.onSourceRespond(mWebComponent);
173         cursor.requery();
174         {
175             final Snapshot snapshot = getSnapshot(cursor);
176             assertTrue(snapshot.isPending);
177             assertEquals(NONE, snapshot.displayNotify);
178             MoreAsserts.assertContentsInOrder("suggestions",
179                     snapshot.suggestionTitles,
180                     mWebSuggestion.getTitle());
181 
182             MoreAsserts.assertContentsInOrder("sources in progress",
183                     mEngine.getPendingSources(),
184                     mComponentA);
185         }
186         mEngine.onSourceRespond(mComponentA);
187         cursor.requery();
188         {
189             final Snapshot snapshot = getSnapshot(cursor);
190             assertFalse(snapshot.isPending);
191 //            assertEquals(NONE, snapshot.displayNotify);   // <--- failing
192             MoreAsserts.assertContentsInOrder("suggestions",
193                     snapshot.suggestionTitles,
194                     mWebSuggestion.getTitle(),
195                     mSuggestionFromA.getTitle(),
196                     mSearchTheWebSuggestion.getTitle());
197 
198             MoreAsserts.assertEmpty("sources in progress", mEngine.getPendingSources());
199         }
200     }
201 
testCaching()202     public void testCaching() {
203         // results for query
204         final Cursor cursor1 = mSession.query("a");
205         mEngine.onSourceRespond(mWebComponent);
206         mEngine.onSourceRespond(mComponentA);
207 
208         // same query again
209         final Cursor cursor2 = mSession.query("a");
210         cursor2.requery();
211         final Snapshot snapshot = getSnapshot(cursor2);
212         assertFalse("should not be pending when results are cached.", snapshot.isPending);
213 //        assertEquals(NONE, snapshot.displayNotify);
214         MoreAsserts.assertContentsInOrder("suggestions",
215                 snapshot.suggestionTitles,
216                 mWebSuggestion.getTitle(),
217                 mSuggestionFromA.getTitle(),
218                 mSearchTheWebSuggestion.getTitle());
219 
220         MoreAsserts.assertEmpty("should be no sources in progress when results are cached.",
221                 mEngine.getPendingSources());
222     }
223 
testErrorResultNotCached()224     public void testErrorResultNotCached() {
225 
226         final TestSuggestionSource aWithError = new TestSuggestionSource.Builder()
227                 .addErrorResponse("a")
228                 .setLabel("A")
229                 .setComponent(mComponentA)
230                 .create();
231 
232         mSession = initSession(Lists.newArrayList(mWebSource, aWithError),
233                 Lists.<SuggestionSource>newArrayList(), mWebSource, 4);
234 
235         {
236             final Cursor cursor = mSession.query("a");
237             mEngine.onSourceRespond(mWebComponent);
238             mEngine.onSourceRespond(mComponentA);
239             cursor.requery();
240 
241             final Snapshot snapshot = getSnapshot(cursor);
242             MoreAsserts.assertContentsInOrder(
243                 snapshot.suggestionTitles,
244                 mWebSuggestion.getTitle(),
245                 mSearchTheWebSuggestion.getTitle());
246         }
247 
248         {
249             final Cursor cursor = mSession.query("a");
250             cursor.requery();
251 
252             final Snapshot snapshot = getSnapshot(cursor);
253             MoreAsserts.assertContentsInOrder(
254                 snapshot.suggestionTitles,
255                 mWebSuggestion.getTitle());
256             MoreAsserts.assertContentsInOrder("expecting source a to be pending (not cached) " +
257                     "since it returned an error the first time.",
258                     mEngine.getPendingSources(),
259                     mComponentA);
260         }
261     }
262 
testSessionClosing_single()263     public void testSessionClosing_single() {
264         final Cursor cursor = mSession.query("a");
265         cursor.close();
266         assertTrue("Session should have closed.", mEngine.isClosed());
267     }
268 
testSessionClosing_multiple()269     public void testSessionClosing_multiple() {
270         // first query fired off
271         final Cursor cursor1 = mSession.query("a");
272         assertFalse("session shouldn't be closed right after opening.", mEngine.isClosed());
273 
274         // second query starts
275         final Cursor cursor2 = mSession.query("b");
276         // first cursor closes (which is how it works from search dialog)
277         cursor1.close();
278         assertFalse("session shouldn't be closed after first cursor close.", mEngine.isClosed());
279 
280         cursor2.close();
281         assertTrue("session should be closed after both cursors closed.", mEngine.isClosed());
282     }
283 
testSessionStats_noClick()284     public void testSessionStats_noClick() {
285         final Cursor cursor = mSession.query("a");
286         cursor.close();
287         final List<SessionStats> stats = mEngine.getSessionStats();
288         MoreAsserts.assertEmpty("session stats reported without click.", stats);
289     }
290 
testSessionStats_click_oneSourceViewed()291     public void testSessionStats_click_oneSourceViewed() {
292         final Cursor cursor = mSession.query("a");
293         mEngine.onSourceRespond(mWebComponent);
294         mEngine.onSourceRespond(mComponentA);
295         cursor.requery();
296         final Snapshot snapshot = getSnapshot(cursor);
297         MoreAsserts.assertContentsInOrder("suggestions.", snapshot.suggestionTitles,
298                 mWebSuggestion.getTitle(), mSuggestionFromA.getTitle(),
299                 mSearchTheWebSuggestion.getTitle());
300 
301         sendClick(cursor, 0, 0);
302         cursor.close();
303         final List<SessionStats> stats = mEngine.getSessionStats();
304         assertEquals("session stats.", 1, stats.size());
305         assertEquals("clicked.", mWebSuggestion, stats.get(0).getClicked());
306         MoreAsserts.assertContentsInAnyOrder("suggestions.", stats.get(0).getSourceImpressions(),
307                 mWebSuggestion.getSource());
308     }
309 
testSessionStats_allSourcesViewed()310     public void testSessionStats_allSourcesViewed() {
311         final Cursor cursor = mSession.query("a");
312         mEngine.onSourceRespond(mWebComponent);
313         mEngine.onSourceRespond(mComponentA);
314         cursor.requery();
315 
316         sendClick(cursor, 1, 1);
317         final List<SessionStats> stats = mEngine.getSessionStats();
318         assertEquals("session stats.", 1, stats.size());
319         assertEquals("clicked.", mSuggestionFromA, stats.get(0).getClicked());
320         MoreAsserts.assertContentsInAnyOrder("sources viewed.", stats.get(0).getSourceImpressions(),
321                 mWebComponent, mComponentA);
322     }
323 
testSessionStats_impressionsWithMoreNotExpanded()324     public void testSessionStats_impressionsWithMoreNotExpanded() {
325         final int numPromotedSources = 1;
326         mSession = initSession(
327                 Lists.newArrayList(mWebSource, mSourceA),
328                 Lists.<SuggestionSource>newArrayList(),
329                 mWebSource,
330                 numPromotedSources);
331 
332         final Cursor cursor = mSession.query("a");
333         mEngine.onSourceRespond(mWebComponent);
334         cursor.requery();
335         final Snapshot snapshot = getSnapshot(cursor);
336         MoreAsserts.assertContentsInOrder("suggestions.", snapshot.suggestionTitles,
337                 mWebSuggestion.getTitle(), mSearchTheWebSuggestion.getTitle(), MORE.getTitle());
338         assertEquals("should want notification of display of index of 'more'",
339                 2, snapshot.displayNotify);
340         sendClick(cursor, 0, 2);
341         final List<SessionStats> stats = mEngine.getSessionStats();
342         assertEquals("session stats.", 1, stats.size());
343         assertEquals("clicked.", mWebSuggestion, stats.get(0).getClicked());
344         MoreAsserts.assertContentsInAnyOrder(
345                 "sources viewed (should not include component of 'more results' suggestion.",
346                 stats.get(0).getSourceImpressions(), mWebComponent);
347     }
348 
349     /**
350      * When the user views a corpus entry under "more results" that hasn't even had a chance to
351      * start running yet, it isn't fair to count an impression with no click against it.
352      */
testSessionStats_impressionsWithMoreExpanded_beforeSourceResponds()353     public void testSessionStats_impressionsWithMoreExpanded_beforeSourceResponds() {
354         final int numPromotedSources = 1;
355         mSession = initSession(
356                 Lists.newArrayList(mWebSource, mSourceA),
357                 Lists.<SuggestionSource>newArrayList(),
358                 mWebSource,
359                 numPromotedSources);
360 
361         final Cursor cursor = mSession.query("a");
362         mEngine.onSourceRespond(mWebComponent);
363         cursor.requery();
364         {
365             final Snapshot snapshot = getSnapshot(cursor);
366             MoreAsserts.assertContentsInOrder("suggestions.", snapshot.suggestionTitles,
367                     mWebSuggestion.getTitle(), mSearchTheWebSuggestion.getTitle(),
368                     MORE.getTitle());
369         }
370 
371         // click on "more"
372         final int selectedPosition = sendClick(cursor, 2, 3);
373         assertEquals("selected position should be index of 'more' after we click on 'more'",
374                 2, selectedPosition);
375         cursor.requery();
376         {
377             final Snapshot snapshot = getSnapshot(cursor);
378             MoreAsserts.assertContentsInOrder("suggestions.",
379                     snapshot.suggestionTitles,
380                     mWebSuggestion.getTitle(),
381                     mSearchTheWebSuggestion.getTitle(),
382                     MORE.getTitle(),
383                     makeCorpusEntry(mComponentA).getTitle());
384             assertFalse("isPending should be false once 'more results' are mixed in.",
385                     snapshot.isPending);
386         }
387 
388         final List<SessionStats> stats = mEngine.getSessionStats();
389         assertEquals("session stats.", 1, stats.size());
390         assertNull("Clicks on More should not be recorded", stats.get(0).getClicked());
391         MoreAsserts.assertContentsInAnyOrder(
392                 "sources viewed (should not include source that was viewed, but hasn't " +
393                         "started retrieving results yet.)",
394                 stats.get(0).getSourceImpressions(), mWebComponent);
395     }
396 
testSessionStats_impressionsWithMoreExpanded_afterSourceResponds()397     public void testSessionStats_impressionsWithMoreExpanded_afterSourceResponds() {
398         final int numPromotedSources = 1;
399         mSession = initSession(
400                 Lists.newArrayList(mWebSource, mSourceA),
401                 Lists.<SuggestionSource>newArrayList(),
402                 mWebSource,
403                 numPromotedSources);
404 
405         final Cursor cursor = mSession.query("a");
406         mEngine.onSourceRespond(mWebComponent);
407         cursor.requery();
408         assertEquals(3, cursor.getCount());
409 
410         // click on "more"
411         int selectedPosition = sendClick(cursor, 2, 2);
412         assertEquals("selected position should be index of 'more' after we click on 'more'",
413                 2, selectedPosition);
414         cursor.requery();
415         assertEquals(4, cursor.getCount());
416 
417         List<SessionStats> stats = mEngine.getSessionStats();
418         assertEquals("session stats.", 1, stats.size());
419         assertNull("Clicks on More should not be recorded", stats.get(0).getClicked());
420         MoreAsserts.assertContentsInAnyOrder(
421                 "sources viewed.",
422                 stats.get(0).getSourceImpressions(), mWebComponent);
423 
424         // viewing that index should kick start the second component
425         final Bundle b = new Bundle();
426         b.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.THRESH_HIT);
427         cursor.respond(b);
428 
429         // source responds
430         mEngine.onSourceRespond(mComponentA);
431         cursor.requery();
432 
433         // click on More to collapse it
434         sendClick(cursor, 2, 3);
435         cursor.requery();
436         assertEquals(3, cursor.getCount());
437 
438         stats = mEngine.getSessionStats();
439         assertEquals("session stats.", 2, stats.size());
440         assertNull("Clicks on More should not be recorded", stats.get(1).getClicked());
441         MoreAsserts.assertContentsInAnyOrder(
442                 "sources viewed.",
443                 stats.get(1).getSourceImpressions(), mWebComponent, mComponentA);
444     }
445 
testSessionStats_search()446     public void testSessionStats_search() {
447         final Cursor cursor = mSession.query("a");
448         mEngine.onSourceRespond(mWebComponent);
449         mEngine.onSourceRespond(mComponentA);
450         cursor.requery();
451 
452         sendSearch(cursor, "a", 1);
453         final List<SessionStats> stats = mEngine.getSessionStats();
454         assertEquals("session stats.", 1, stats.size());
455         SuggestionData searchSuggestion = createWebSearchShortcut("a");
456         assertEquals("clicked.", searchSuggestion, stats.get(0).getClicked());
457         MoreAsserts.assertContentsInAnyOrder("sources viewed.", stats.get(0).getSourceImpressions(),
458                 mWebComponent, mComponentA);
459     }
460 
461 // --------------------- Utility methods ---------------------
462 
sendClick(Cursor cursor, int position, int maxDisplayedPosition)463     private int sendClick(Cursor cursor, int position, int maxDisplayedPosition) {
464         final Bundle b = new Bundle();
465         b.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.CLICK);
466         b.putInt(DialogCursorProtocol.CLICK_SEND_POSITION, position);
467         b.putInt(DialogCursorProtocol.CLICK_SEND_MAX_DISPLAY_POS, maxDisplayedPosition);
468         final Bundle response = cursor.respond(b);
469         return response.getInt(DialogCursorProtocol.CLICK_RECEIVE_SELECTED_POS, -1);
470     }
471 
sendSearch(Cursor cursor, String query, int maxDisplayedPosition)472     private void sendSearch(Cursor cursor, String query, int maxDisplayedPosition) {
473         final Bundle b = new Bundle();
474         b.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.SEARCH);
475         b.putString(DialogCursorProtocol.SEARCH_SEND_QUERY, query);
476         b.putInt(DialogCursorProtocol.SEARCH_SEND_MAX_DISPLAY_POS, maxDisplayedPosition);
477         cursor.respond(b);
478     }
479 
480     /**
481      * @param cursor A cursor
482      * @return A snapshot of information contained in that cursor.
483      */
getSnapshot(Cursor cursor)484     private Snapshot getSnapshot(Cursor cursor) {
485         final ArrayList<String> titles = new ArrayList<String>(cursor.getCount());
486 
487         if (!cursor.isBeforeFirst()) {
488             cursor.moveToPosition(-1);
489         }
490 
491         while (cursor.moveToNext()) {
492             titles.add(cursor.getString(
493                     cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)));
494         }
495 
496         final Bundle bundleIn = new Bundle();
497         bundleIn.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.POST_REFRESH);
498         final Bundle bundleOut = cursor.respond(bundleIn);
499 
500         return new Snapshot(
501                 titles,
502                 bundleOut.getBoolean(DialogCursorProtocol.POST_REFRESH_RECEIVE_ISPENDING),
503                 bundleOut.getInt(
504                         DialogCursorProtocol.POST_REFRESH_RECEIVE_DISPLAY_NOTIFY,
505                         NONE));
506     }
507 
508     static class Snapshot {
509         final ArrayList<String> suggestionTitles;
510         final boolean isPending;
511         final int displayNotify;
512 
Snapshot(ArrayList<String> suggestionTitles, boolean pending, int displayNotify)513         Snapshot(ArrayList<String> suggestionTitles, boolean pending, int displayNotify) {
514             this.suggestionTitles = suggestionTitles;
515             isPending = pending;
516             this.displayNotify = displayNotify;
517         }
518     }
519 
520     static final int NONE = -1;
521 
522     /**
523      * Utility class to instrument the plumbing of {@link SuggestionSession} so we can
524      * control how results are reported and processed, and keep track of when the session is
525      * closed.
526      */
527     static class QueryEngine extends PerTagExecutor implements Executor, DelayedExecutor,
528             SuggestionSession.SessionCallback, ShortcutRepository {
529 
530         private long mNow = 0L;
531 
532         private final LinkedHashMap<ComponentName, FutureTask<SuggestionResult>> mPending
533                 = new LinkedHashMap<ComponentName, FutureTask<SuggestionResult>>();
534 
535         private LinkedList<Delayed> mDelayed = new LinkedList<Delayed>();
536 
537         private boolean mClosed = false;
538 
QueryEngine()539         public QueryEngine() {
540             super(null, 66);
541         }
542 
543         /**
544          * book keeping for delayed runnables for emulating delayed execution.
545          */
546         private static class Delayed {
547             final long start;
548             final long delay;
549             final Runnable runnable;
550 
Delayed(long start, long delay, Runnable runnable)551             Delayed(long start, long delay, Runnable runnable) {
552                 this.start = start;
553                 this.delay = delay;
554                 this.runnable = runnable;
555             }
556         }
557 
558         private List<SessionStats> mSessionStats = new ArrayList<SessionStats>();
559 
560         /**
561          * @return A list of sources that have been queried and haven't been triggered to respond
562          *         via {@link #onSourceRespond(android.content.ComponentName)}
563          */
getPendingSources()564         public List<ComponentName> getPendingSources() {
565             return new ArrayList<ComponentName>(mPending.keySet());
566         }
567 
568         /**
569          * Simulate a source responding.
570          *
571          * @param source The source to have respond.
572          * @return The result of the response for further inspection.
573          */
onSourceRespond(ComponentName source)574         public SuggestionResult onSourceRespond(ComponentName source) {
575             final FutureTask<SuggestionResult> task = mPending.remove(source);
576             if (task == null) {
577                 throw new IllegalArgumentException(source + " never started");
578             }
579             task.run();
580             try {
581                 return task.get();
582             } catch (Exception e) {
583                 throw new RuntimeException(e);
584             }
585         }
586 
587         /**
588          * Runs all pending source tasks.  This can be useful when starting a new
589          * query, to get to a consistent state before more assertions.
590          */
finishAllSourceTasks()591         public void finishAllSourceTasks() {
592             for (FutureTask<SuggestionResult> task : mPending.values()) {
593                 task.run();
594             }
595             mPending.clear();
596         }
597 
598         /**
599          * Moves time forward the specified number of milliseconds, executing any tasks
600          * that were posted to {@link #postDelayed(Runnable, long)} as appropriate.
601          *
602          * @param millis
603          */
moveTimeForward(long millis)604         public void moveTimeForward(long millis) {
605             mNow += millis;
606             List<Runnable> toRun = new ArrayList<Runnable>();
607             final Iterator<Delayed> it = mDelayed.iterator();
608             while (it.hasNext()) {
609                 Delayed delayed = it.next();
610                 if (mNow >= delayed.start + delayed.delay) {
611                     it.remove();
612                     toRun.add(delayed.runnable);
613                 }
614             }
615 
616             // do this in a separate pass to avoid concurrent modification of list,
617             // since these runnables might add more to the queue
618             for (Runnable runnable : toRun) {
619                 runnable.run();
620             }
621         }
622 
623         // ShortcutRepository
624 
hasHistory()625         public boolean hasHistory() {return true;}
clearHistory()626         public void clearHistory() {}
deleteRepository()627         public void deleteRepository() {}
close()628         public void close() {}
reportStats(SessionStats stats)629         public void reportStats(SessionStats stats) {
630             mSessionStats.add(stats);
631         }
632 
getShortcutsForQuery(String query)633         public ArrayList<SuggestionData> getShortcutsForQuery(String query) {
634             return new ArrayList<SuggestionData>();
635         }
636 
getSourceRanking()637         public ArrayList<ComponentName> getSourceRanking() {
638             throw new IllegalArgumentException();
639         }
refreshShortcut( ComponentName source, String shortcutId, SuggestionData refreshed)640         public void refreshShortcut(
641                 ComponentName source, String shortcutId, SuggestionData refreshed) {}
642 
643         /**
644          * @return The stats that have been reported
645          */
getSessionStats()646         public List<SessionStats> getSessionStats() {
647             return mSessionStats;
648         }
649 
650         // Executor
651 
652         @Override
execute(String tag, Runnable command)653         public boolean execute(String tag, Runnable command) {
654             execute(command);
655             return false;
656         }
657 
execute(Runnable command)658         public void execute(Runnable command) {
659             if (command instanceof QueryMultiplexer.SuggestionRequest) {
660                 final QueryMultiplexer.SuggestionRequest suggestionRequest =
661                         (QueryMultiplexer.SuggestionRequest) command;
662                 mPending.put(
663                         suggestionRequest.getSuggestionSource().getComponentName(),
664                         suggestionRequest);
665             } else {
666                 command.run();
667             }
668         }
669 
670         // DelayedExecutor
671 
postDelayed(Runnable runnable, long delayMillis)672         public void postDelayed(Runnable runnable, long delayMillis) {
673             mDelayed.add(new Delayed(mNow, delayMillis, runnable));
674         }
675 
postAtTime(Runnable runnable, long uptimeMillis)676         public void postAtTime(Runnable runnable, long uptimeMillis) {runnable.run();}
677 
678 
679         // Session callback
680 
closeSession()681         public void closeSession() {
682             mClosed = true;
683         }
684 
isClosed()685         public boolean isClosed() {
686             return mClosed;
687         }
688     }
689 
690     static class TestSuggestionSession extends SuggestionSession {
691         private final QueryEngine mEngine;
692 
TestSuggestionSession(Config config, SourceLookup sourceLookup, ArrayList<SuggestionSource> promotableSources, ArrayList<SuggestionSource> unpromotableSources, SuggestionSessionTest test, QueryEngine engine, int numPromotedSources)693         public TestSuggestionSession(Config config, SourceLookup sourceLookup,
694                 ArrayList<SuggestionSource> promotableSources,
695                 ArrayList<SuggestionSource> unpromotableSources,
696                 SuggestionSessionTest test,
697                 QueryEngine engine, int numPromotedSources) {
698             super(config, sourceLookup, promotableSources, unpromotableSources,
699                     engine, engine, engine, test, true);
700             setListener(engine);
701             setShortcutRepo(engine);
702             setNumPromotedSources(numPromotedSources);
703             mEngine = engine;
704         }
705 
706         @Override
getNow()707         long getNow() {
708             return mEngine.mNow;
709         }
710     }
711 }
712