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