• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 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.quicksearchbox;
18 
19 import com.android.quicksearchbox.util.MockExecutor;
20 import com.android.quicksearchbox.util.Util;
21 
22 import android.app.SearchManager;
23 import android.test.AndroidTestCase;
24 import android.test.MoreAsserts;
25 import android.test.suitebuilder.annotation.MediumTest;
26 import android.util.Log;
27 
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.Comparator;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Map.Entry;
36 
37 import junit.framework.Assert;
38 
39 /**
40  * Abstract base class for tests of  {@link ShortcutRepository}
41  * implementations.  Most importantly, verifies the
42  * stuff we are doing with sqlite works how we expect it to.
43  *
44  * Attempts to test logic independent of the (sql) details of the implementation, so these should
45  * be useful even in the face of a schema change.
46  */
47 @MediumTest
48 public class ShortcutRepositoryTest extends AndroidTestCase {
49 
50     private static final String TAG = "ShortcutRepositoryTest";
51 
52     static final long NOW = 1239841162000L; // millis since epoch. some time in 2009
53 
54     static final Source APP_SOURCE = new MockSource("com.example.app/.App");
55 
56     static final Source APP_SOURCE_V2 = new MockSource("com.example.app/.App", 2);
57 
58     static final Source CONTACTS_SOURCE = new MockSource("com.android.contacts/.Contacts");
59 
60     static final Source BOOKMARKS_SOURCE = new MockSource("com.android.browser/.Bookmarks");
61 
62     static final Source HISTORY_SOURCE = new MockSource("com.android.browser/.History");
63 
64     static final Source MUSIC_SOURCE = new MockSource("com.android.music/.Music");
65 
66     static final Source MARKET_SOURCE = new MockSource("com.android.vending/.Market");
67 
68     static final Corpus APP_CORPUS = new MockCorpus(APP_SOURCE);
69 
70     static final Corpus CONTACTS_CORPUS = new MockCorpus(CONTACTS_SOURCE);
71 
72     static final int MAX_SHORTCUTS = 8;
73 
74     protected Config mConfig;
75     protected MockCorpora mCorpora;
76     protected MockExecutor mLogExecutor;
77     protected ShortcutRefresher mRefresher;
78 
79     protected List<Corpus> mAllowedCorpora;
80 
81     protected ShortcutRepositoryImplLog mRepo;
82 
83     protected ListSuggestionCursor mAppSuggestions;
84     protected ListSuggestionCursor mContactSuggestions;
85 
86     protected SuggestionData mApp1;
87     protected SuggestionData mApp2;
88     protected SuggestionData mApp3;
89 
90     protected SuggestionData mContact1;
91     protected SuggestionData mContact2;
92 
createShortcutRepository()93     protected ShortcutRepositoryImplLog createShortcutRepository() {
94         return new ShortcutRepositoryImplLog(getContext(), mConfig, mCorpora,
95                 mRefresher, new MockHandler(), mLogExecutor,
96                 "test-shortcuts-log.db").disableUpdateDelay();
97     }
98 
99     @Override
setUp()100     protected void setUp() throws Exception {
101         super.setUp();
102 
103         mConfig = new Config(getContext());
104         mCorpora = new MockCorpora();
105         mCorpora.addCorpus(APP_CORPUS);
106         mCorpora.addCorpus(CONTACTS_CORPUS);
107         mRefresher = new MockShortcutRefresher();
108         mLogExecutor = new MockExecutor();
109         mRepo = createShortcutRepository();
110 
111         mAllowedCorpora = new ArrayList<Corpus>(mCorpora.getAllCorpora());
112 
113         mApp1 = makeApp("app1");
114         mApp2 = makeApp("app2");
115         mApp3 = makeApp("app3");
116         mAppSuggestions = new ListSuggestionCursor("foo", mApp1, mApp2, mApp3);
117 
118         mContact1 = new SuggestionData(CONTACTS_SOURCE)
119                 .setText1("Joe Blow")
120                 .setIntentAction("view")
121                 .setIntentData("contacts/joeblow")
122                 .setShortcutId("j-blow");
123         mContact2 = new SuggestionData(CONTACTS_SOURCE)
124                 .setText1("Mike Johnston")
125                 .setIntentAction("view")
126                 .setIntentData("contacts/mikeJ")
127                 .setShortcutId("mo-jo");
128 
129         mContactSuggestions = new ListSuggestionCursor("foo", mContact1, mContact2);
130     }
131 
makeApp(String name)132     private SuggestionData makeApp(String name) {
133         return new SuggestionData(APP_SOURCE)
134                 .setText1(name)
135                 .setIntentAction("view")
136                 .setIntentData("apps/" + name)
137                 .setShortcutId("shorcut_" + name);
138     }
139 
makeContact(String name)140     private SuggestionData makeContact(String name) {
141         return new SuggestionData(CONTACTS_SOURCE)
142                 .setText1(name)
143                 .setIntentAction("view")
144                 .setIntentData("contacts/" + name)
145                 .setShortcutId("shorcut_" + name);
146     }
147 
148     @Override
tearDown()149     protected void tearDown() throws Exception {
150         super.tearDown();
151         mRepo.deleteRepository();
152     }
153 
testHasHistory()154     public void testHasHistory() {
155         assertFalse(mRepo.hasHistory());
156         reportClickAtTime(mAppSuggestions, 0, NOW);
157         assertTrue(mRepo.hasHistory());
158         mRepo.clearHistory();
159         assertTrue(mRepo.hasHistory());
160         mLogExecutor.runNext();
161         assertFalse(mRepo.hasHistory());
162     }
163 
testNoMatch()164     public void testNoMatch() {
165         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
166                 .setText1("bob smith")
167                 .setIntentAction("action")
168                 .setIntentData("data");
169 
170         reportClick("bob smith", clicked);
171         assertNoShortcuts("joe");
172     }
173 
testFullPackingUnpacking()174     public void testFullPackingUnpacking() {
175         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
176                 .setFormat("<i>%s</i>")
177                 .setText1("title")
178                 .setText2("description")
179                 .setText2Url("description_url")
180                 .setIcon1("android.resource://system/drawable/foo")
181                 .setIcon2("content://test/bar")
182                 .setIntentAction("action")
183                 .setIntentData("data")
184                 .setSuggestionQuery("query")
185                 .setIntentExtraData("extradata")
186                 .setShortcutId("idofshortcut")
187                 .setSuggestionLogType("logtype");
188         reportClick("q", clicked);
189 
190         assertShortcuts("q", clicked);
191         assertShortcuts("", clicked);
192     }
193 
testSpinnerWhileRefreshing()194     public void testSpinnerWhileRefreshing() {
195         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
196                 .setText1("title")
197                 .setText2("description")
198                 .setIcon2("icon2")
199                 .setSuggestionQuery("query")
200                 .setIntentExtraData("extradata")
201                 .setShortcutId("idofshortcut")
202                 .setSpinnerWhileRefreshing(true);
203 
204         reportClick("q", clicked);
205 
206         String spinnerUri = Util.getResourceUri(mContext, R.drawable.search_spinner).toString();
207         SuggestionData expected = new SuggestionData(CONTACTS_SOURCE)
208                 .setText1("title")
209                 .setText2("description")
210                 .setIcon2(spinnerUri)
211                 .setSuggestionQuery("query")
212                 .setIntentExtraData("extradata")
213                 .setShortcutId("idofshortcut")
214                 .setSpinnerWhileRefreshing(true);
215 
216         assertShortcuts("q", expected);
217     }
218 
testPrefixesMatch()219     public void testPrefixesMatch() {
220         assertNoShortcuts("bob");
221 
222         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
223                 .setText1("bob smith the third")
224                 .setIntentAction("action")
225                 .setIntentData("intentdata");
226 
227         reportClick("bob smith", clicked);
228 
229         assertShortcuts("bob smith", clicked);
230         assertShortcuts("bob s", clicked);
231         assertShortcuts("b", clicked);
232     }
233 
testMatchesOneAndNotOthers()234     public void testMatchesOneAndNotOthers() {
235         SuggestionData bob = new SuggestionData(CONTACTS_SOURCE)
236                 .setText1("bob smith the third")
237                 .setIntentAction("action")
238                 .setIntentData("intentdata/bob");
239 
240         reportClick("bob", bob);
241 
242         SuggestionData george = new SuggestionData(CONTACTS_SOURCE)
243                 .setText1("george jones")
244                 .setIntentAction("action")
245                 .setIntentData("intentdata/george");
246         reportClick("geor", george);
247 
248         assertShortcuts("b for bob", "b", bob);
249         assertShortcuts("g for george", "g", george);
250     }
251 
testDifferentPrefixesMatchSameEntity()252     public void testDifferentPrefixesMatchSameEntity() {
253         SuggestionData clicked = new SuggestionData(CONTACTS_SOURCE)
254                 .setText1("bob smith the third")
255                 .setIntentAction("action")
256                 .setIntentData("intentdata");
257 
258         reportClick("bob", clicked);
259         reportClick("smith", clicked);
260         assertShortcuts("b", clicked);
261         assertShortcuts("s", clicked);
262     }
263 
testMoreClicksWins()264     public void testMoreClicksWins() {
265         reportClick("app", mApp1);
266         reportClick("app", mApp2);
267         reportClick("app", mApp1);
268 
269         assertShortcuts("expected app1 to beat app2 since it has more hits",
270                 "app", mApp1, mApp2);
271 
272         reportClick("app", mApp2);
273         reportClick("app", mApp2);
274 
275         assertShortcuts("query 'app': expecting app2 to beat app1 since it has more hits",
276                 "app", mApp2, mApp1);
277         assertShortcuts("query 'a': expecting app2 to beat app1 since it has more hits",
278                 "a", mApp2, mApp1);
279     }
280 
testMostRecentClickWins()281     public void testMostRecentClickWins() {
282         // App 1 has 3 clicks
283         reportClick("app", mApp1, NOW - 5);
284         reportClick("app", mApp1, NOW - 5);
285         reportClick("app", mApp1, NOW - 5);
286         // App 2 has 2 clicks
287         reportClick("app", mApp2, NOW - 2);
288         reportClick("app", mApp2, NOW - 2);
289         // App 3 only has 1, but it's most recent
290         reportClick("app", mApp3, NOW - 1);
291 
292         assertShortcuts("expected app3 to beat app1 and app2 because it's clicked last",
293                 "app", mApp3, mApp1, mApp2);
294 
295         reportClick("app", mApp2, NOW);
296 
297         assertShortcuts("query 'app': expecting app2 to beat app1 since it's clicked last",
298                 "app", mApp2, mApp1, mApp3);
299         assertShortcuts("query 'a': expecting app2 to beat app1 since it's clicked last",
300                 "a", mApp2, mApp1, mApp3);
301         assertShortcuts("query '': expecting app2 to beat app1 since it's clicked last",
302                 "", mApp2, mApp1, mApp3);
303     }
304 
testMostRecentClickWinsOnEmptyQuery()305     public void testMostRecentClickWinsOnEmptyQuery() {
306         reportClick("app", mApp1, NOW - 3);
307         reportClick("app", mApp1, NOW - 2);
308         reportClick("app", mApp2, NOW - 1);
309 
310         assertShortcuts("expected app2 to beat app1 since it's clicked last", "",
311                 mApp2, mApp1);
312     }
313 
testMostRecentClickWinsEvenWithMoreThanLimitShortcuts()314     public void testMostRecentClickWinsEvenWithMoreThanLimitShortcuts() {
315         for (int i = 0; i < MAX_SHORTCUTS; i++) {
316             SuggestionData app = makeApp("TestApp" + i);
317             // Each of these shortcuts has two clicks
318             reportClick("app", app, NOW - 2);
319             reportClick("app", app, NOW - 1);
320         }
321 
322         // mApp1 has only one click, but is more recent
323         reportClick("app", mApp1, NOW);
324 
325         assertShortcutAtPosition(
326             "expecting app1 to beat all others since it's clicked last",
327             "app", 0, mApp1);
328     }
329 
330     /**
331      * similar to {@link #testMoreClicksWins()} but clicks are reported with prefixes of the
332      * original query.  we want to make sure a match on query 'a' updates the stats for the
333      * entry it matched against, 'app'.
334      */
testPrefixMatchUpdatesSameEntry()335     public void testPrefixMatchUpdatesSameEntry() {
336         reportClick("app", mApp1, NOW);
337         reportClick("app", mApp2, NOW);
338         reportClick("app", mApp1, NOW);
339 
340         assertShortcuts("expected app1 to beat app2 since it has more hits",
341                 "app", mApp1, mApp2);
342     }
343 
344     private static final long DAY_MILLIS = 86400000L; // just ask the google
345     private static final long HOUR_MILLIS = 3600000L;
346 
testMoreRecentlyClickedWins()347     public void testMoreRecentlyClickedWins() {
348         reportClick("app", mApp1, NOW - DAY_MILLIS*2);
349         reportClick("app", mApp2, NOW);
350         reportClick("app", mApp3, NOW - DAY_MILLIS*4);
351 
352         assertShortcuts("expecting more recently clicked app to rank higher",
353                 "app", mApp2, mApp1, mApp3);
354     }
355 
testMoreRecentlyClickedWinsSeconds()356     public void testMoreRecentlyClickedWinsSeconds() {
357         reportClick("app", mApp1, NOW - 10000);
358         reportClick("app", mApp2, NOW - 5000);
359         reportClick("app", mApp3, NOW);
360 
361         assertShortcuts("expecting more recently clicked app to rank higher",
362                 "app", mApp3, mApp2, mApp1);
363     }
364 
testRecencyOverridesClicks()365     public void testRecencyOverridesClicks() {
366 
367         // 5 clicks, most recent half way through age limit
368         long halfWindow = mConfig.getMaxStatAgeMillis() / 2;
369         reportClick("app", mApp1, NOW - halfWindow);
370         reportClick("app", mApp1, NOW - halfWindow);
371         reportClick("app", mApp1, NOW - halfWindow);
372         reportClick("app", mApp1, NOW - halfWindow);
373         reportClick("app", mApp1, NOW - halfWindow);
374 
375         // 3 clicks, the most recent very recent
376         reportClick("app", mApp2, NOW - HOUR_MILLIS);
377         reportClick("app", mApp2, NOW - HOUR_MILLIS);
378         reportClick("app", mApp2, NOW - HOUR_MILLIS);
379 
380         assertShortcuts("expecting 3 recent clicks to beat 5 clicks long ago",
381                 "app", mApp2, mApp1);
382     }
383 
testEntryOlderThanAgeLimitFiltered()384     public void testEntryOlderThanAgeLimitFiltered() {
385         reportClick("app", mApp1);
386 
387         long pastWindow = mConfig.getMaxStatAgeMillis() + 1000;
388         reportClick("app", mApp2, NOW - pastWindow);
389 
390         assertShortcuts("expecting app2 not clicked on recently enough to be filtered",
391                 "app", mApp1);
392     }
393 
testZeroQueryResults_MoreClicksWins()394     public void testZeroQueryResults_MoreClicksWins() {
395         reportClick("app", mApp1);
396         reportClick("app", mApp1);
397         reportClick("foo", mApp2);
398 
399         assertShortcuts("", mApp1, mApp2);
400 
401         reportClick("foo", mApp2);
402         reportClick("foo", mApp2);
403 
404         assertShortcuts("", mApp2, mApp1);
405     }
406 
testZeroQueryResults_DifferentQueryhitsCreditSameShortcut()407     public void testZeroQueryResults_DifferentQueryhitsCreditSameShortcut() {
408         reportClick("app", mApp1);
409         reportClick("foo", mApp2);
410         reportClick("bar", mApp2);
411 
412         assertShortcuts("hits for 'foo' and 'bar' on app2 should have combined to rank it " +
413                 "ahead of app1, which only has one hit.",
414                 "", mApp2, mApp1);
415 
416         reportClick("z", mApp1);
417         reportClick("2", mApp1);
418 
419         assertShortcuts("", mApp1, mApp2);
420     }
421 
testZeroQueryResults_zeroQueryHitCounts()422     public void testZeroQueryResults_zeroQueryHitCounts() {
423         reportClick("app", mApp1);
424         reportClick("", mApp2);
425         reportClick("", mApp2);
426 
427         assertShortcuts("hits for '' on app2 should have combined to rank it " +
428                 "ahead of app1, which only has one hit.",
429                 "", mApp2, mApp1);
430 
431         reportClick("", mApp1);
432         reportClick("", mApp1);
433 
434         assertShortcuts("zero query hits for app1 should have made it higher than app2.",
435                 "", mApp1, mApp2);
436 
437         assertShortcuts("query for 'a' should only match app1.",
438                 "a", mApp1);
439     }
440 
testRefreshShortcut()441     public void testRefreshShortcut() {
442         final SuggestionData app1 = new SuggestionData(APP_SOURCE)
443                 .setFormat("format")
444                 .setText1("app1")
445                 .setText2("cool app")
446                 .setShortcutId("app1_id");
447 
448         reportClick("app", app1);
449 
450         final SuggestionData updated = new SuggestionData(APP_SOURCE)
451                 .setFormat("format (updated)")
452                 .setText1("app1 (updated)")
453                 .setText2("cool app")
454                 .setShortcutId("app1_id");
455 
456         refreshShortcut(APP_SOURCE, "app1_id", updated);
457 
458         assertShortcuts("expected updated properties in match",
459                 "app", updated);
460     }
461 
testRefreshShortcutChangedIntent()462     public void testRefreshShortcutChangedIntent() {
463 
464         final SuggestionData app1 = new SuggestionData(APP_SOURCE)
465                 .setIntentData("data")
466                 .setFormat("format")
467                 .setText1("app1")
468                 .setText2("cool app")
469                 .setShortcutId("app1_id");
470 
471         reportClick("app", app1);
472 
473         final SuggestionData updated = new SuggestionData(APP_SOURCE)
474                 .setIntentData("data-updated")
475                 .setFormat("format (updated)")
476                 .setText1("app1 (updated)")
477                 .setText2("cool app")
478                 .setShortcutId("app1_id");
479 
480         refreshShortcut(APP_SOURCE, "app1_id", updated);
481 
482         assertShortcuts("expected updated properties in match",
483                 "app", updated);
484     }
485 
testInvalidateShortcut()486     public void testInvalidateShortcut() {
487         final SuggestionData app1 = new SuggestionData(APP_SOURCE)
488                 .setText1("app1")
489                 .setText2("cool app")
490                 .setShortcutId("app1_id");
491 
492         reportClick("app", app1);
493 
494         invalidateShortcut(APP_SOURCE, "app1_id");
495 
496         assertNoShortcuts("should be no matches since shortcut is invalid.", "app");
497     }
498 
testInvalidateShortcut_sameIdDifferentSources()499     public void testInvalidateShortcut_sameIdDifferentSources() {
500         final String sameid = "same_id";
501         final SuggestionData app = new SuggestionData(APP_SOURCE)
502                 .setText1("app1")
503                 .setText2("cool app")
504                 .setShortcutId(sameid);
505         reportClick("app", app);
506         assertShortcuts("app should be there", "", app);
507 
508         final SuggestionData contact = new SuggestionData(CONTACTS_SOURCE)
509                 .setText1("joe blow")
510                 .setText2("a good pal")
511                 .setShortcutId(sameid);
512         reportClick("joe", contact);
513         reportClick("joe", contact);
514         assertShortcuts("app and contact should be there.", "", contact, app);
515 
516         refreshShortcut(APP_SOURCE, sameid, null);
517         assertNoShortcuts("app should not be there.", "app");
518         assertShortcuts("contact with same shortcut id should still be there.",
519                 "joe", contact);
520         assertShortcuts("contact with same shortcut id should still be there.",
521                 "", contact);
522     }
523 
testNeverMakeShortcut()524     public void testNeverMakeShortcut() {
525         final SuggestionData contact = new SuggestionData(CONTACTS_SOURCE)
526                 .setText1("unshortcuttable contact")
527                 .setText2("you didn't want to call them again anyway")
528                 .setShortcutId(SearchManager.SUGGEST_NEVER_MAKE_SHORTCUT);
529         reportClick("unshortcuttable", contact);
530         assertNoShortcuts("never-shortcutted suggestion should not be there.", "unshortcuttable");
531     }
532 
testCountResetAfterShortcutDeleted()533     public void testCountResetAfterShortcutDeleted() {
534         reportClick("app", mApp1);
535         reportClick("app", mApp1);
536         reportClick("app", mApp1);
537         reportClick("app", mApp1);
538 
539         reportClick("app", mApp2);
540         reportClick("app", mApp2);
541 
542         // app1 wins 4 - 2
543         assertShortcuts("app", mApp1, mApp2);
544 
545         // reset to 1
546         invalidateShortcut(APP_SOURCE, mApp1.getShortcutId());
547         reportClick("app", mApp1);
548 
549         // app2 wins 2 - 1
550         assertShortcuts("expecting app1's click count to reset after being invalidated.",
551                 "app", mApp2, mApp1);
552     }
553 
testShortcutsAllowedCorpora()554     public void testShortcutsAllowedCorpora() {
555         reportClick("a", mApp1);
556         reportClick("a", mContact1);
557 
558         assertShortcuts("only allowed shortcuts should be returned",
559                 "a", Arrays.asList(APP_CORPUS), mApp1);
560     }
561 
562     //
563     // SOURCE RANKING TESTS BELOW
564     //
565 
testSourceRanking_moreClicksWins()566     public void testSourceRanking_moreClicksWins() {
567         assertCorpusRanking("expected no ranking");
568 
569         int minClicks = mConfig.getMinClicksForSourceRanking();
570 
571         // click on an app
572         for (int i = 0; i < minClicks + 1; i++) {
573             reportClick("a", mApp1);
574         }
575         // fewer clicks on a contact
576         for (int i = 0; i < minClicks; i++) {
577             reportClick("a", mContact1);
578         }
579 
580         assertCorpusRanking("expecting apps to rank ahead of contacts (more clicks)",
581                 APP_CORPUS, CONTACTS_CORPUS);
582 
583         // more clicks on a contact
584         reportClick("a", mContact1);
585         reportClick("a", mContact1);
586 
587         assertCorpusRanking("expecting contacts to rank ahead of apps (more clicks)",
588                 CONTACTS_CORPUS, APP_CORPUS);
589     }
590 
testOldSourceStatsDontCount()591     public void testOldSourceStatsDontCount() {
592         // apps were popular back in the day
593         final long toOld = mConfig.getMaxStatAgeMillis() + 1;
594         int minClicks = mConfig.getMinClicksForSourceRanking();
595         for (int i = 0; i < minClicks; i++) {
596             reportClick("app", mApp1, NOW - toOld);
597         }
598 
599         // and contacts is 1/2
600         for (int i = 0; i < minClicks; i++) {
601             reportClick("bob", mContact1, NOW);
602         }
603 
604         assertCorpusRanking("old clicks for apps shouldn't count.",
605                 CONTACTS_CORPUS);
606     }
607 
608 
testSourceRanking_filterSourcesWithInsufficientData()609     public void testSourceRanking_filterSourcesWithInsufficientData() {
610         int minClicks = mConfig.getMinClicksForSourceRanking();
611         // not enough
612         for (int i = 0; i < minClicks - 1; i++) {
613             reportClick("app", mApp1);
614         }
615         // just enough
616         for (int i = 0; i < minClicks; i++) {
617             reportClick("bob", mContact1);
618         }
619 
620         assertCorpusRanking(
621                 "ordering should only include sources with at least " + minClicks + " clicks.",
622                 CONTACTS_CORPUS);
623     }
624 
625     // App upgrade tests
626 
testAppUpgradeClearsShortcuts()627     public void testAppUpgradeClearsShortcuts() {
628         reportClick("a", mApp1);
629         reportClick("add", mApp1);
630         reportClick("a", mContact1);
631 
632         assertShortcuts("all shortcuts should be returned",
633                 "a", mAllowedCorpora, mApp1, mContact1);
634 
635         // Upgrade an existing corpus
636         MockCorpus upgradedCorpus = new MockCorpus(APP_SOURCE_V2);
637         mCorpora.addCorpus(upgradedCorpus);
638 
639         List<Corpus> newAllowedCorpora = new ArrayList<Corpus>(mCorpora.getAllCorpora());
640         assertShortcuts("app shortcuts should be removed when the source was upgraded",
641                 "a", newAllowedCorpora, mContact1);
642     }
643 
testAppUpgradePromotesLowerRanked()644     public void testAppUpgradePromotesLowerRanked() {
645 
646         ListSuggestionCursor expected = new ListSuggestionCursor("a");
647         for (int i = 0; i < MAX_SHORTCUTS + 1; i++) {
648             reportClick("app", mApp1, NOW);
649         }
650         expected.add(mApp1);
651 
652         // Enough contact clicks to make one more shortcut than getMaxShortcutsReturned()
653         for (int i = 0; i < MAX_SHORTCUTS; i++) {
654             SuggestionData contact = makeContact("andy" + i);
655             int numClicks = MAX_SHORTCUTS - i;  // use click count to get shortcuts in order
656             for (int j = 0; j < numClicks; j++) {
657                 reportClick("and", contact, NOW);
658             }
659             expected.add(contact);
660         }
661 
662         // Expect the app, and then all but one contact
663         assertShortcuts("app and all but one contact should be returned",
664                 "a", mAllowedCorpora, SuggestionCursorUtil.slice(expected, 0, MAX_SHORTCUTS));
665 
666         // Upgrade app corpus
667         MockCorpus upgradedCorpus = new MockCorpus(APP_SOURCE_V2);
668         mCorpora.addCorpus(upgradedCorpus);
669 
670         // Expect all contacts
671         List<Corpus> newAllowedCorpora = new ArrayList<Corpus>(mCorpora.getAllCorpora());
672         assertShortcuts("app shortcuts should be removed when the source was upgraded "
673                 + "and a contact should take its place",
674                 "a", newAllowedCorpora, SuggestionCursorUtil.slice(expected, 1, MAX_SHORTCUTS));
675     }
676 
testIrrelevantAppUpgrade()677     public void testIrrelevantAppUpgrade() {
678         reportClick("a", mApp1);
679         reportClick("add", mApp1);
680         reportClick("a", mContact1);
681 
682         assertShortcuts("all shortcuts should be returned",
683                 "a", mAllowedCorpora, mApp1, mContact1);
684 
685         // Fire a corpus set update that affect no shortcuts corpus
686         MockCorpus newCorpus = new MockCorpus(new MockSource("newsource"));
687         mCorpora.addCorpus(newCorpus);
688 
689         assertShortcuts("all shortcuts should be returned",
690                 "a", mAllowedCorpora, mApp1, mContact1);
691     }
692 
693     // Utilities
694 
makeCursor(String query, SuggestionData... suggestions)695     protected ListSuggestionCursor makeCursor(String query, SuggestionData... suggestions) {
696         ListSuggestionCursor cursor = new ListSuggestionCursor(query);
697         for (SuggestionData suggestion : suggestions) {
698             cursor.add(suggestion);
699         }
700         return cursor;
701     }
702 
reportClick(String query, SuggestionData suggestion)703     protected void reportClick(String query, SuggestionData suggestion) {
704         reportClick(new ListSuggestionCursor(query, suggestion), 0);
705     }
706 
reportClick(String query, SuggestionData suggestion, long now)707     protected void reportClick(String query, SuggestionData suggestion, long now) {
708         reportClickAtTime(new ListSuggestionCursor(query, suggestion), 0, now);
709     }
710 
reportClick(SuggestionCursor suggestions, int position)711     protected void reportClick(SuggestionCursor suggestions, int position) {
712         reportClickAtTime(suggestions, position, NOW);
713     }
714 
reportClickAtTime(SuggestionCursor suggestions, int position, long now)715     protected void reportClickAtTime(SuggestionCursor suggestions, int position, long now) {
716         mRepo.reportClickAtTime(suggestions, position, now);
717         mLogExecutor.runNext();
718     }
719 
invalidateShortcut(Source source, String shortcutId)720     protected void invalidateShortcut(Source source, String shortcutId) {
721         refreshShortcut(source, shortcutId, null);
722     }
723 
refreshShortcut(Source source, String shortcutId, SuggestionData suggestion)724     protected void refreshShortcut(Source source, String shortcutId, SuggestionData suggestion) {
725         SuggestionCursor refreshed =
726                 suggestion == null ? null : new ListSuggestionCursor(null, suggestion);
727         mRepo.refreshShortcut(source, shortcutId, refreshed);
728         mLogExecutor.runNext();
729     }
730 
sourceImpressions(Source source, int clicks, int impressions)731     protected void sourceImpressions(Source source, int clicks, int impressions) {
732         if (clicks > impressions) throw new IllegalArgumentException("ya moran!");
733 
734         for (int i = 0; i < impressions; i++, clicks--) {
735             sourceImpression(source, clicks > 0);
736         }
737     }
738 
739     /**
740      * Simulate an impression, and optionally a click, on a source.
741      *
742      * @param source The name of the source.
743      * @param click Whether to register a click in addition to the impression.
744      */
sourceImpression(Source source, boolean click)745     protected void sourceImpression(Source source, boolean click) {
746         sourceImpression(source, click, NOW);
747     }
748 
749     /**
750      * Simulate an impression, and optionally a click, on a source.
751      *
752      * @param source The name of the source.
753      * @param click Whether to register a click in addition to the impression.
754      */
sourceImpression(Source source, boolean click, long now)755     protected void sourceImpression(Source source, boolean click, long now) {
756         SuggestionData suggestionClicked = !click ?
757                 null :
758                 new SuggestionData(source)
759                     .setIntentAction("view")
760                     .setIntentData("data/id")
761                     .setShortcutId("shortcutid");
762 
763         reportClick("a", suggestionClicked);
764     }
765 
assertNoShortcuts(String query)766     void assertNoShortcuts(String query) {
767         assertNoShortcuts("", query);
768     }
769 
assertNoShortcuts(String message, String query)770     void assertNoShortcuts(String message, String query) {
771         SuggestionCursor cursor = mRepo.getShortcutsForQuery(query, mAllowedCorpora, NOW);
772         try {
773             assertNull(message + ", got shortcuts", cursor);
774         } finally {
775             if (cursor != null) cursor.close();
776         }
777     }
778 
assertShortcuts(String query, SuggestionData... expected)779     void assertShortcuts(String query, SuggestionData... expected) {
780         assertShortcuts("", query, expected);
781     }
782 
assertShortcutAtPosition(String message, String query, int position, SuggestionData expected)783     void assertShortcutAtPosition(String message, String query,
784             int position, SuggestionData expected) {
785         SuggestionCursor cursor = mRepo.getShortcutsForQuery(query, mAllowedCorpora, NOW);
786         try {
787             SuggestionCursor expectedCursor = new ListSuggestionCursor(query, expected);
788             SuggestionCursorUtil.assertSameSuggestion(message, position, expectedCursor, cursor);
789         } finally {
790             if (cursor != null) cursor.close();
791         }
792     }
793 
assertShortcutCount(String message, String query, int expectedCount)794     void assertShortcutCount(String message, String query, int expectedCount) {
795         SuggestionCursor cursor = mRepo.getShortcutsForQuery(query, mAllowedCorpora, NOW);
796         try {
797             assertEquals(message, expectedCount, cursor.getCount());
798         } finally {
799             if (cursor != null) cursor.close();
800         }
801     }
802 
assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora, SuggestionCursor expected)803     void assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora,
804             SuggestionCursor expected) {
805         SuggestionCursor cursor = mRepo.getShortcutsForQuery(query, allowedCorpora, NOW);
806         try {
807             SuggestionCursorUtil.assertSameSuggestions(message, expected, cursor, true);
808         } finally {
809             if (cursor != null) cursor.close();
810         }
811     }
812 
assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora, SuggestionData... expected)813     void assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora,
814             SuggestionData... expected) {
815         assertShortcuts(message, query, allowedCorpora, new ListSuggestionCursor(query, expected));
816     }
817 
assertShortcuts(String message, String query, SuggestionData... expected)818     void assertShortcuts(String message, String query, SuggestionData... expected) {
819         assertShortcuts(message, query, mAllowedCorpora, expected);
820     }
821 
assertCorpusRanking(String message, Corpus... expected)822     void assertCorpusRanking(String message, Corpus... expected) {
823         String[] expectedNames = new String[expected.length];
824         for (int i = 0; i < expected.length; i++) {
825             expectedNames[i] = expected[i].getName();
826         }
827         Map<String,Integer> scores = mRepo.getCorpusScores();
828         List<String> observed = sortByValues(scores);
829         // Highest scores should come first
830         Collections.reverse(observed);
831         Log.d(TAG, "scores=" + scores);
832         assertContentsInOrder(message, observed, (Object[]) expectedNames);
833     }
834 
sortByValues(Map<A,B> map)835     static <A extends Comparable<A>, B extends Comparable<B>> List<A> sortByValues(Map<A,B> map) {
836         Comparator<Map.Entry<A,B>> comp = new Comparator<Map.Entry<A,B>>() {
837             public int compare(Entry<A, B> object1, Entry<A, B> object2) {
838                 int diff = object1.getValue().compareTo(object2.getValue());
839                 if (diff != 0) {
840                     return diff;
841                 } else {
842                     return object1.getKey().compareTo(object2.getKey());
843                 }
844             }
845         };
846         ArrayList<Map.Entry<A,B>> sorted = new ArrayList<Map.Entry<A,B>>(map.size());
847         sorted.addAll(map.entrySet());
848         Collections.sort(sorted, comp);
849         ArrayList<A> out = new ArrayList<A>(sorted.size());
850         for (Map.Entry<A,B> e : sorted) {
851             out.add(e.getKey());
852         }
853         return out;
854     }
855 
assertContentsInOrder(Iterable<?> actual, Object... expected)856     static void assertContentsInOrder(Iterable<?> actual, Object... expected) {
857         assertContentsInOrder(null, actual, expected);
858     }
859 
860     /**
861      * an implementation of {@link MoreAsserts#assertContentsInOrder(String, Iterable, Object[])}
862      * that isn't busted.  a bug has been filed about that, but for now this works.
863      */
assertContentsInOrder( String message, Iterable<?> actual, Object... expected)864     static void assertContentsInOrder(
865             String message, Iterable<?> actual, Object... expected) {
866         ArrayList actualList = new ArrayList();
867         for (Object o : actual) {
868             actualList.add(o);
869         }
870         Assert.assertEquals(message, Arrays.asList(expected), actualList);
871     }
872 }
873