Bug 1396282 - Add query for getting Highlights (recent bookmarks and recent history with metadata). r=mak
authorEd Lee <edilee@mozilla.com>
Sat, 02 Sep 2017 13:15:04 -0700
changeset 428719 fc91951971ecb71886540056d630a468a5e04635
parent 428718 73c7371abb7f25f819ec26438265d625fe6e5a7a
child 428720 3adbca67c67a33c46b14256b879adfdc7b207909
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1396282
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1396282 - Add query for getting Highlights (recent bookmarks and recent history with metadata). r=mak Adds index to moz_bookmarks.dateAdded for use by Highlights query for recent bookmarks. MozReview-Commit-ID: 7Gs8H0kUij2
toolkit/components/places/Database.cpp
toolkit/components/places/Database.h
toolkit/components/places/nsPlacesIndexes.h
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/migration/places_v39.sqlite
toolkit/components/places/tests/migration/xpcshell.ini
toolkit/modules/NewTabUtils.jsm
toolkit/modules/tests/xpcshell/test_NewTabUtils.js
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -1106,18 +1106,26 @@ Database::InitSchema(bool* aDatabaseMigr
       }
 
       // Firefox 55 uses schema version 37.
 
       if (currentSchemaVersion < 38) {
         rv = MigrateV38Up();
         NS_ENSURE_SUCCESS(rv, rv);
       }
+
       // Firefox 56 uses schema version 38.
 
+      if (currentSchemaVersion < 39) {
+        rv = MigrateV39Up();
+        NS_ENSURE_SUCCESS(rv, rv);
+      }
+
+      // Firefox 57 uses schema version 39.
+
       // Schema Upgrades must add migration code here.
 
       rv = UpdateBookmarkRootTitles();
       // We don't want a broken localization to cause us to think
       // the database is corrupt and needs to be replaced.
       MOZ_ASSERT(NS_SUCCEEDED(rv));
     }
   }
@@ -1164,16 +1172,18 @@ Database::InitSchema(bool* aDatabaseMigr
     rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_BOOKMARKS_DELETED);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_PLACETYPE);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_PARENTPOSITION);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_PLACELASTMODIFIED);
     NS_ENSURE_SUCCESS(rv, rv);
+    rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_DATEADDED);
+    NS_ENSURE_SUCCESS(rv, rv);
     rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_GUID);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // moz_keywords.
     rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_KEYWORDS);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_KEYWORDS_PLACEPOSTDATA);
     NS_ENSURE_SUCCESS(rv, rv);
@@ -2320,16 +2330,27 @@ Database::MigrateV38Up()
     ));
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   return NS_OK;
 }
 
 nsresult
+Database::MigrateV39Up() {
+  MOZ_ASSERT(NS_IsMainThread());
+
+  // Create an index on dateAdded.
+  nsresult rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_DATEADDED);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
+nsresult
 Database::GetItemsWithAnno(const nsACString& aAnnoName, int32_t aItemType,
                            nsTArray<int64_t>& aItemIds)
 {
   nsCOMPtr<mozIStorageStatement> stmt;
   nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
     "SELECT b.id FROM moz_items_annos a "
     "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id "
     "JOIN moz_bookmarks b ON b.id = a.item_id "
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -14,17 +14,17 @@
 #include "mozilla/storage/StatementCache.h"
 #include "mozilla/Attributes.h"
 #include "nsIEventTarget.h"
 #include "Shutdown.h"
 #include "nsCategoryCache.h"
 
 // This is the schema version. Update it at any schema change and add a
 // corresponding migrateVxx method below.
-#define DATABASE_SCHEMA_VERSION 38
+#define DATABASE_SCHEMA_VERSION 39
 
 // Fired after Places inited.
 #define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
 // This topic is received when the profile is about to be lost.  Places does
 // initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
 // Any shutdown work that requires the Places APIs should happen here.
 #define TOPIC_PROFILE_CHANGE_TEARDOWN "profile-change-teardown"
 // Fired when Places is shutting down.  Any code should stop accessing Places
@@ -298,16 +298,17 @@ protected:
   nsresult MigrateV31Up();
   nsresult MigrateV32Up();
   nsresult MigrateV33Up();
   nsresult MigrateV34Up();
   nsresult MigrateV35Up();
   nsresult MigrateV36Up();
   nsresult MigrateV37Up();
   nsresult MigrateV38Up();
+  nsresult MigrateV39Up();
 
   nsresult UpdateBookmarkRootTitles();
 
   friend class ConnectionShutdownBlocker;
 
   int64_t CreateMobileRoot();
   nsresult GetItemsWithAnno(const nsACString& aAnnoName, int32_t aItemType,
                             nsTArray<int64_t>& aItemIds);
--- a/toolkit/components/places/nsPlacesIndexes.h
+++ b/toolkit/components/places/nsPlacesIndexes.h
@@ -84,16 +84,21 @@
     "parentindex", "moz_bookmarks", "parent, position", "" \
   )
 
 #define CREATE_IDX_MOZ_BOOKMARKS_PLACELASTMODIFIED \
   CREATE_PLACES_IDX( \
     "itemlastmodifiedindex", "moz_bookmarks", "fk, lastModified", "" \
   )
 
+#define CREATE_IDX_MOZ_BOOKMARKS_DATEADDED \
+  CREATE_PLACES_IDX( \
+    "dateaddedindex", "moz_bookmarks", "dateAdded", "" \
+  )
+
 #define CREATE_IDX_MOZ_BOOKMARKS_GUID \
   CREATE_PLACES_IDX( \
     "guid_uniqueindex", "moz_bookmarks", "guid", "UNIQUE" \
   )
 
 /**
  * moz_annos
  */
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -1,17 +1,17 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // It is expected that the test files importing this file define Cu etc.
 /* global Cu, Ci, Cc, Cr */
 
-const CURRENT_SCHEMA_VERSION = 38;
+const CURRENT_SCHEMA_VERSION = 39;
 const FIRST_UPGRADABLE_SCHEMA_VERSION = 11;
 
 const NS_APP_USER_PROFILE_50_DIR = "ProfD";
 const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
 
 // Shortcuts to transitions type.
 const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
 const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..65fc6f0109747e92258e549bce90153fbc32365d
GIT binary patch
literal 1146880
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
z$J;mVKztqczx-Mv`myNHjX%5bLpQ$d`WLR(uHXOi@5ey^00000000000000000000
z0000000000000000O0GvQ~etgXA&Fxa(#V?t+{L>(fVWY?`{4~_QxN6cRj2%9v^w+
z-Ic<Xvq62n+^9c3vU_;vRHZupXk+j0Mrk^c_{&Gej(v8=`*Ir-2Ud>L8esn|{m(pI
zIeq4lWA%NPPG4;HfAsf$>a{a{8xw~Td;0qNQgQ#DL}I4Vn5(~c*RFV+YS0+2gx5;t
za$z{EP4Akng~Nq%xpXb83^#&mp*%O7cy%hA9y@n<UwC2c@YRO~PhP+KUFF_QiJQHN
z2Ud<*DNRh28pD%eW!Dq+pmsT^JzlC!cF*zd$(cg68k8U3`tY7=X?mtnzFG+<ha1g~
zm3X#^<71akOkb={T{(E>p}{9E-TR)0vl|l+CK3yi#T!tV2<IE`ovan=GeJG^>e;bt
zV^3cUr-J#D&m|Hc`H7#+|H|tx^~5W@d$qGJ6pLZCzU-`n>dvuqyQWIDU@E-QPS~}u
zw4IHh-Uw<t8(~<kFWa7aH7phj@q!N=J~&f6G;w)$Pj&A@gRky>dh}vuW8!GyvG&}_
zuDOfB<)9qS1+_Idy%x-c<v6Pt4}5I=T=Ch$?6oV;pWl~yXz<5>c;;izrZ*;zB=)Tw
zd18LLJ>Xgcx60}HiRKPh>a~mGmma>jXa8gIxS#pb^Y4FuYGdM5;)#{JHBk<yhZpu@
z&9SeR8#B|T>hN?}ZjOKa$nK|4Rh~L^;^L|O4-J0o&6~gdJ;{xU@x;!R!@v0Ai>rJS
zlVP<{D^1Kdf@RY*lM}D*e)Q=1{bMH%&z2r}?4iM_-~8xH1I@ebl{<X50{{R300000
z00000000000000000000000000002M9X8NBymE&Z0RR9100000000000000000000
z000000000000000cUXV(@X8%t1ONa400000000000000000000000000000000000
z++hRpPjB>OBKouFSEHYfUXA9W?}@7b00000000000000000000000000000000000
z008j!^7h_rVkFg<m@5~mL1VZQUMrQ$h2gL^op^OHn@wzq+bX4riBe;DGOQ$CeS1$k
zzfdfO)%x=M`!d<YzPPOzTn@_NTu@uyd~Z6N*c&%b%ulzY%UbiPY~t}|YdM@Au9l`}
z8s)3YGww=e6Av_((;e)O&!IOeCZa!yJ{|pB^x^0SqL<<-00000000000000000000
z0000000000000000002|KlknFk>p&tPz@TxmGD}rTrLcUwdvjs=?8LM^$qDQ$x3Nr
zqSP3k3@h1t()*Id;Brt7=Ym>yzs%dxdy^CM)9pZ4L;BwI<H?C~I6Yh~P0uvSSGyZi
z%|#T7#jslM4)+Yi>l=LO6N%`zqSvB}(YtSa@y5q*T)Q!H{Y%$>=KAFI`(OV3mp}aS
zcfXu}>3_sY00000000000000000000000000000000000008iHYRBM)L^6@g#UHcT
zt+`w}mFT$`yf*mh-JP;}Z07Xkr;p8Ed9-kC|K<&eTr%1BKs!6xpE?^%2GvIXd||rY
z>agkF*V`RG{>kylXZKus=FB4p_KX}(J+`UcbL&FST>3;<Z{(jX)k}?_*y^(3jo;}E
zF?s2kt20MR7xp|AoKAh$##Yzl_Ey)vWM5zQbnCC6=TEf%0C}s|`rU7|%ST=>kJJyH
zsf>@D9vi)oI=rFX`>}=Iy?s50OZCb5dOd%#{pZc|VKsmB?4fq=9bf2-_RP6PW9Q}Z
z@zV63tEu-5v`5*wFiN&BbD&(#Uk&GL`N>+LJ`=Q0fBm+vb;>g*pE~{Am1EEDpPD?I
zI=H^wfA2#7p1yQv_VYo#5!CYM!?3(C#@0=pF;0z^>$M{<yzu<USR-|0U3-it7RJc*
zr4}|If4K7p-t#Ae#*1NXwmraoPqxb~+jgIPVfwj!js2B#7pBus_O}Mg?O7No-PalC
z;QVy{e627!8`Ro8@446+sWAWib2G=zT)Hqjc{qI_*Zwv;r<3Y?s#FW6!Yhk=+Wh<R
z?XGvvb;|Rj2g^qeUE5te_VB*ck-pZ2w|F+GRO;3-&Ia{(e<wRzyZM8iuckD8;c(;l
z=-8zb7cceg?~T_IUr<{+XLBUJGLBX+hvmx)7uNdRCpsgXIC|woqj2Wr%+bg8rXJ2V
zmyk=|vvBRFGL>+mR1TWSAA0kFnNHb`j<<YQPrJ`U3)gWfeX21N@B6_poUIgUv-R%I
zudVyL#C&xs?p$njPVLULd*9ueG1a+oy60=evGE0ur&~R8yE^kNPSm<$CJMD>6UO&Y
zqaAMb?s;#j)xB@~%I+tE>in|4@x5M&W1X3M9!fU*_Vo>P7JF-UEeso-?1ARP<dr-7
zECT=l00000000000000000000000000000000000fIF?fd3faxF9HAn0000000000
z00000000000000000000000000Pe7X_$L+pSt9zY_y7O^00000000000000000000
z0000000000000000N?y_sh-4L$wZ}C&GaRE5}T8WiEHWpxFHpNB@z8id;kCd00000
z000000000000000000000000000000fNvSS>ARAZVl~y1-kh7bHV{XW(U%hOe*gdg
z00000000000000000000000000000000000@GW9fGQBlf39pUM23KDUYsGqMAdaS^
zpH4)79DOP-000000000000000000000000000000000000002s>)lW~o!EQ2T$l{%
zyUtY#wZ_3PoQ=!bEva<k=)^+hbS<0<YK>ByR1E5qwbERp6jq0lao<WfQ7Q+!I+^ve
zVc6)82Tn&nlZgH}`czy100000000000000000000000000000000000006+(y`l8p
zTqT?+m4jUqVK`eU)Mo2v!?5wr^wDG|aJm-G1+_*gsP8HU^~qXku2BlBskf!~CQp|O
zlX3W5oYpwFke(iB4*bd;J~03Q000000000000000000000000000000000000Jy^j
z;-5^kEfM`mbUiv7e*gdg00000000000000000000000000000000000fZN!X=}#n+
zTl$j8Y<6?{Y%m#A8|Mqt^-Ma^b1`^r@Z+ECl-twI%m-STo6;x3dgIwrz0?Sb3#ps#
zeZ5mYpIR5E-fZWlQwNI0{6rYeRtmM*dcG0n8#ASPekv>%gIcp%4}v%pRPx1AeXd-%
z8Wi(kwHe7DT<mo|49gRR+QMAxdw!!+-kpq>mD|{t%w;m!k<5qQd|;;eGZ3HK!1Wu6
z=s!mviR#hOXv>YS-uUE=AG~qq#{Jj7eEna?O#lD@0000000000000000000000000
z0000000000{~tU&klFgoU6t_K_-t_X#jsYa*Iy`?8o_vBz7dwH#o)?#cjNfT!}*c#
zT%Xx`X{bBAEM=}-m<;B^dZSoq6vpSPr5EOd)<|*J{K)XS%!A)~PqV|~ADUlQ8ILEg
zw9-fR=O4~Lyt_a1;1jpYpDWaYYNONRk^IPebD0Ovz10lmLcLK5i>0YjQ0$s*q%ZT}
z=-s#J8=q59C>Faq?di=tc=UE%rsqq=?rn%Wjl3tD+46X+Q){>COP|K)Yvu8oLVc#4
zyE{Mf?w-t+y{qTW#Jk+ddw5$Wvt{4Vs(F`7^-^OpoUb;T1MYrLdd=CVYQbbsoxIu^
zb?@#}X3LS)M{Vw5YvkspEG~EV&SYlGq18uUu@!NCe|#18M?aW|z8d{$^o8iN(SL}3
zIr@0?lhKdHK>z>%00000000000000000000000000000000000xD(c=v&lr_FCQ5@
z_Sqf%oyw!X_fxOs+Le#|#Lwn`<@IcTx+nS2;Ag({{QJ}Gj~{#U=5MFkAAkIZXFir`
ze|&ZK)1y7PbWiS~!KvT;=u7S3$?JE&tFQg>iA(psr?)?zwm*705&doS)#%I7XQN+;
zek}Td=yG&14gvrG00000000000000000000000000000000000z~9rx^aIICI62%X
zO-z(3!@c*VpGZ#E!r?->T)GxkhOd?zGt;H&@N`(77*5@l-kIxeeeuN?hlkR8mi1@^
z)k1l0xbN0@y&4vah2fsJrypAuuLW~qIk+4Yhcox2_bqE`mDBSR!|9&%#^i8+>zK==
z6Ul6Uy!QU+*Avm3(O*YjiT*tLV)Td6=cC_^J`)E40000000000000000000000000
z0000000000007_{QzqS)>~9`&%|l=F(Azv@n}?p}A=5mhn}<|7-Iwc+JM~AuoQVE5
z`m5-FMSl|gLG*jkXQE$={(T$-00000000000000000000000000000000000004k*
z4B2!lnRvCYUG%n#Y`f@b7nydEZWpPZbSf7I(|yVQ<{{S~57Hm~dLnu=`s?T`(Vs_O
zjQ%kCeDu4~XW}3L00000000000000000000000000000000000004Yr%A^y?{^lXq
zJoGgWz0E_mdFW{#GR;G}c}S(xiCll&sXw}qh`t>Ce)ONBUy6Pv`sdLPL|3DkI0yg$
z000000000000000000000000000000000000RMM3rMD)h!s)SdhxdgS#tvWYYX&AN
z_1eYpOAlY%v;VPlD{y@5@`>q-)u}57&-69}XUDFMJ$*5p3g%Bfmu&{lJY6|`=8<Fd
zeV0yO%ryhYkL-T>ROP8tCoZ1apK1n<jh`z%TbR9e<@xjbGR?q&!v|-IhbAu1?y2tW
z*_hs%+x_U#^ZUn69G)#b@>nxr=TxOS{%B+G?nY_4zrVTKSMKm`0RR910000000000
z000000000000000000000002M9oFAGymE&Z0RR910000000000000000000000000
z0000000000cUXV(@X8%t1ONa400000000000000000000000000000000000++qFk
z&tUX@iRf>lzli>4^xvb;NB<@Ijp$dSe;55?^z+fjqMwXD8htqWXVE{2lK=n!00000
z000000000000000000000000000000;2%&nlguUiH@Ax1rdH9nu~qbLXcgIkR?)M*
zRb<w+igbUgNacDmxi~YMNhFi$!B&y#YZuve(bFz6?IPVSQay2zOZCU|4@R#fqQ8y4
z8httXV)Wml&qco#eLDJ7^h?pdiT+jeFQbn}KN9`Z=!0<*00000000000000000000
z0000000000000000002|1L@5qlgYm9#&+4Wp<QMM+GTouyG*UiW|FyNUw>TmCHr!%
zTDrGYOK)oBq*8sYXlk$(O>NF*a>-;S+bYsMts<4_$>idW@iETGWcvDY{qY(GZ@ig^
zz7qXW^xM%J(Jw?l8+|1Dq3C;~MifNPMn|Ik(e~)=(YiPZ00000000000000000000
z000000000000000004k*N*glCY_dPmb1`^r@Z+D{+^XhU)lK)lzNuC1YgN}j^7_VB
zwYOc}{l<n?HQTOk``SRO+S947Z&fqx>XvQmTGe#Bdf$`%t!k=Wy?ZV<kjZ6}?b_xK
z_6}qc3$?*d?;c!MORbM<xlV1VX~P@8lU}ODtLs~;t^2z~&tmOEZ$2=ST~^EV$NMlC
z4JD$#jQ%+KT=Z+vC!?Q>{zdfR=+!8SYEckfjGl?!7mY;kijx2U000000000000000
z000000000000000000000Qgq3KAlY_dM*a94Sss};9_Ody{~UxtZaDWcQ!3n*6)5}
z<5FeE7d9+awta12sj_v``lZT!Pp(_4+;g#isdD#RZmF{QgMCYt+$VZFl@Go7z)ZHg
z($ihZbXU^dmDIX)HrJ{2$0s{@<JS|>m!r=|zZU&s^wZIgM*ldv5=}?XMx)Uq(F4(7
zl)CZ1Z+sz60ssI20000000000000000000000000000000002s8^xyd*5rJ3s#Fe&
z<4rxyKqCyx6NOqUkZuJE)AjKMnT_eKxmp-DI)P?F_1Mhm%TFJhz4B<`+F&zK2`Az)
z+X>lLAgIoFdZe0x@<{#AnacRc>9NrZ8=HaB^o7HX<D+AjPF%dy*9=TvdgkiPk<x`d
zPX(tpH3NnD=bxK7cIMKB*~!EG&A|BNvwJQ*bLNo)dqxg#ZU)Ye9xNX@bZvL>*u(pB
z&A^$HPn~}5%CTqnPfea3Xa-J=mg}`6FTC*l$XH{2GjQVQl@pD^nUga|AKTm83_NqL
z(b#!;e7rQh=jw)LVE4%vrk~r_*k3t!VR~JEylJWEFA~w;#0LNX000000000000000
z000000000000000000000QlCC%_I}aOsXd?a_RnN<dr+TA^-pY000000000000000
z00000000000000000000xWoG6pG@=ziRf$5SE4_SKL7v#00000000000000000000
q000000000000000z&EAdOfs3=oX)h$P3d$`CYj4+TeWm5^?v}7<5>a#
--- a/toolkit/components/places/tests/migration/xpcshell.ini
+++ b/toolkit/components/places/tests/migration/xpcshell.ini
@@ -19,16 +19,17 @@ support-files =
   places_v31.sqlite
   places_v32.sqlite
   places_v33.sqlite
   places_v34.sqlite
   places_v35.sqlite
   places_v36.sqlite
   places_v37.sqlite
   places_v38.sqlite
+  places_v39.sqlite
 
 [test_current_from_downgraded.js]
 [test_current_from_v6.js]
 [test_current_from_v11.js]
 [test_current_from_v19.js]
 [test_current_from_v24.js]
 [test_current_from_v25.js]
 [test_current_from_v26.js]
--- a/toolkit/modules/NewTabUtils.jsm
+++ b/toolkit/modules/NewTabUtils.jsm
@@ -54,16 +54,19 @@ const LINKS_GET_LINKS_LIMIT = 100;
 const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
 
 // Some default frecency threshold for Activity Stream requests
 const ACTIVITY_STREAM_DEFAULT_FRECENCY = 150;
 
 // Some default query limit for Activity Stream requests
 const ACTIVITY_STREAM_DEFAULT_LIMIT = 12;
 
+// Some default seconds ago for Activity Stream recent requests
+const ACTIVITY_STREAM_DEFAULT_RECENT = 5 * 24 * 60 * 60;
+
 /**
  * Calculate the MD5 hash for a string.
  * @param aValue
  *        The string to convert.
  * @return The base64 representation of the MD5 hash.
  */
 function toHash(aValue) {
   let value = gUnicodeConverter.convertToByteArray(aValue);
@@ -872,16 +875,38 @@ var ActivityStreamProvider = {
     return Object.assign({
       bookmarkType: PlacesUtils.bookmarks.TYPE_BOOKMARK,
       limit: this._adjustLimitForBlocked(aOptions),
       tagsFolderId: PlacesUtils.tagsFolderId
     }, aParams);
   },
 
   /**
+   * Shared columns for Highlights related queries.
+   */
+  _highlightsColumns: ["bookmarkGuid", "description", "guid",
+    "preview_image_url", "title", "url"],
+
+  /**
+   * Shared post-processing of Highlights links.
+   */
+  _processHighlights(aLinks, aOptions, aType) {
+    // Filter out blocked if necessary
+    if (!aOptions.ignoreBlocked) {
+      aLinks = aLinks.filter(link => !BlockedLinks.isBlocked(link));
+    }
+
+    // Limit the results to the requested number and set a type corresponding to
+    // which query selected it
+    return aLinks.slice(0, aOptions.numItems).map(item => Object.assign(item, {
+      type: aType
+    }));
+  },
+
+  /**
    * From an Array of links, if favicons are present, convert to data URIs
    *
    * @param {Array} aLinks
    *          an array containing objects with favicon data and mimeTypes
    *
    * @returns {Array} an array of links with favicons as data uri
    */
   _faviconBytesToDataURI(aLinks) {
@@ -937,16 +962,96 @@ var ActivityStreamProvider = {
           link.mimeType = null;
           return link;
         })
       ));
     }
     return aLinks;
   },
 
+  /**
+   * Get most-recently-created visited bookmarks for Activity Stream.
+   *
+   * @param {Object} aOptions
+   *   {num}  bookmarkSecondsAgo: Maximum age of added bookmark.
+   *   {bool} ignoreBlocked: Do not filter out blocked links.
+   *   {int}  numItems: Maximum number of items to return.
+   */
+  async getRecentBookmarks(aOptions) {
+    const options = Object.assign({
+      bookmarkSecondsAgo: ACTIVITY_STREAM_DEFAULT_RECENT,
+      ignoreBlocked: false,
+      numItems: ACTIVITY_STREAM_DEFAULT_LIMIT
+    }, aOptions || {});
+
+    const sqlQuery = `
+      SELECT
+        b.guid AS bookmarkGuid,
+        description,
+        h.guid,
+        preview_image_url,
+        b.title,
+        url
+      FROM moz_bookmarks b
+      JOIN moz_bookmarks p
+        ON p.id = b.parent
+      JOIN moz_places h
+        ON h.id = b.fk
+      WHERE b.dateAdded >= :dateAddedThreshold
+        AND b.title NOTNULL
+        AND b.type = :bookmarkType
+        AND p.parent <> :tagsFolderId
+        ${this._commonPlacesWhere}
+      ORDER BY b.dateAdded DESC
+      LIMIT :limit
+    `;
+
+    return this._processHighlights(await this.executePlacesQuery(sqlQuery, {
+      columns: this._highlightsColumns,
+      params: this._getCommonParams(options, {
+        dateAddedThreshold: (Date.now() - options.bookmarkSecondsAgo * 1000) * 1000
+      })
+    }), options, "bookmark");
+  },
+
+  /**
+   * Get most-recently-visited history with metadata for Activity Stream.
+   *
+   * @param {Object} aOptions
+   *   {bool} ignoreBlocked: Do not filter out blocked links.
+   *   {int}  numItems: Maximum number of items to return.
+   */
+  async getRecentHistory(aOptions) {
+    const options = Object.assign({
+      ignoreBlocked: false,
+      numItems: ACTIVITY_STREAM_DEFAULT_LIMIT,
+    }, aOptions || {});
+
+    const sqlQuery = `
+      SELECT
+        ${this._commonBookmarkGuidSelect},
+        description,
+        guid,
+        preview_image_url,
+        title,
+        url
+      FROM moz_places h
+      WHERE description NOTNULL
+        AND preview_image_url NOTNULL
+        ${this._commonPlacesWhere}
+      ORDER BY last_visit_date DESC
+      LIMIT :limit
+    `;
+
+    return this._processHighlights(await this.executePlacesQuery(sqlQuery, {
+      columns: this._highlightsColumns,
+      params: this._getCommonParams(options)
+    }), options, "history");
+  },
+
   /*
    * Gets the top frecent sites for Activity Stream.
    *
    * @param {Object} aOptions
    *   {bool} ignoreBlocked: Do not filter out blocked links.
    *   {int}  numItems: Maximum number of items to return.
    *   {int}  topsiteFrecency: Minimum amount of frecency for a site.
    *
@@ -1176,16 +1281,57 @@ var ActivityStreamLinks = {
    */
   deleteHistoryEntry(aUrl) {
     const url = aUrl;
     PinnedLinks.unpin({url});
     return PlacesUtils.history.remove(url);
   },
 
   /**
+   * Get the Highlights links to show on Activity Stream
+   *
+   * @param {Object} aOptions
+   *   {bool} excludeBookmarks: Don't add bookmark items.
+   *   {bool} excludeHistory: Don't add history items.
+   *   {int}  numItems: Maximum number of (bookmark or history) items to return.
+   *
+   * @return {Promise} Returns a promise with the array of links as the payload
+   */
+  async getHighlights(aOptions = {}) {
+    aOptions.numItems = aOptions.numItems || ACTIVITY_STREAM_DEFAULT_LIMIT;
+    const results = [];
+
+    // First get bookmarks if we want them
+    if (!aOptions.excludeBookmarks) {
+      results.push(...await ActivityStreamProvider.getRecentBookmarks(aOptions));
+    }
+
+    // Add in history if we need more and want them
+    if (aOptions.numItems - results.length > 0 && !aOptions.excludeHistory) {
+      // Use the same numItems as bookmarks above in case we remove duplicates
+      const history = await ActivityStreamProvider.getRecentHistory(aOptions);
+
+      // Only include a url once in the result preferring the bookmark
+      const bookmarkUrls = new Set(results.map(({url}) => url));
+      for (const page of history) {
+        if (!bookmarkUrls.has(page.url)) {
+          results.push(page);
+
+          // Stop adding pages once we reach the desired maximum
+          if (results.length === aOptions.numItems) {
+            break;
+          }
+        }
+      }
+    }
+
+    return results;
+  },
+
+  /**
    * Get the top sites to show on Activity Stream
    *
    * @return {Promise} Returns a promise with the array of links as the payload
    */
   async getTopSites(aOptions = {}) {
     return ActivityStreamProvider.getTopFrecentSites(aOptions);
   }
 };
--- a/toolkit/modules/tests/xpcshell/test_NewTabUtils.js
+++ b/toolkit/modules/tests/xpcshell/test_NewTabUtils.js
@@ -359,16 +359,129 @@ add_task(async function addFavicons() {
   await PlacesTestUtils.addFavicons(faviconData);
 
   await provider._addFavicons(links);
   Assert.equal(links[0].mimeType, "image/png", "Got the right mime type before deleting it");
   Assert.equal(links[0].faviconLength, links[0].favicon.length, "Got the right length for the byte array");
   Assert.equal(provider._faviconBytesToDataURI(links)[0].favicon, base64URL, "Got the right favicon");
 });
 
+add_task(async function getHighlights() {
+  const addMetadata = url => PlacesUtils.history.update({
+    description: "desc",
+    previewImageURL: "https://image/",
+    url
+  });
+
+  await setUpActivityStreamTest();
+
+  let provider = NewTabUtils.activityStreamLinks;
+  let links = await provider.getHighlights();
+  Assert.equal(links.length, 0, "empty history yields empty links");
+
+  // Add bookmarks
+  const now = Date.now();
+  const oldSeconds = 24 * 60 * 60; // 1 day old
+  let bookmarks = [
+    {
+      dateAdded: new Date(now - oldSeconds * 1000),
+      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+      title: "foo",
+      url: "https://mozilla1.com/dayOld"
+    },
+    {
+      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+      title: "foo",
+      url: "https://mozilla1.com/nowNew"
+    }
+  ];
+  for (let placeInfo of bookmarks) {
+    await PlacesUtils.bookmarks.insert(placeInfo);
+  }
+
+  links = await provider.getHighlights();
+  Assert.equal(links.length, 0, "adding bookmarks without visits doesn't yield more links");
+
+  // Add a history visit
+  let testURI = "http://mozilla.com/";
+  await PlacesTestUtils.addVisits(testURI);
+
+  links = await provider.getHighlights();
+  Assert.equal(links.length, 0, "adding visits without metadata doesn't yield more links");
+
+  // Add bookmark visits
+  for (let placeInfo of bookmarks) {
+    await PlacesTestUtils.addVisits(placeInfo.url);
+  }
+
+  links = await provider.getHighlights();
+  Assert.equal(links.length, 2, "adding visits to bookmarks yields more links");
+  Assert.equal(links[0].url, bookmarks[1].url, "first bookmark is younger bookmark");
+  Assert.equal(links[0].type, "bookmark", "first bookmark is bookmark");
+  Assert.equal(links[1].url, bookmarks[0].url, "second bookmark is older bookmark");
+  Assert.equal(links[1].type, "bookmark", "second bookmark is bookmark");
+
+  // Add metadata to history
+  await addMetadata(testURI);
+
+  links = await provider.getHighlights();
+  Assert.equal(links.length, 3, "adding metadata yield more links");
+  Assert.equal(links[0].url, bookmarks[1].url, "still have younger bookmark");
+  Assert.equal(links[1].url, bookmarks[0].url, "still have older bookmark");
+  Assert.equal(links[2].url, testURI, "added visit corresponds to added url");
+  Assert.equal(links[2].type, "history", "added visit is history");
+
+  links = await provider.getHighlights({numItems: 2});
+  Assert.equal(links.length, 2, "limited to 2 items");
+  Assert.equal(links[0].url, bookmarks[1].url, "still have younger bookmark");
+  Assert.equal(links[1].url, bookmarks[0].url, "still have older bookmark");
+
+  links = await provider.getHighlights({excludeHistory: true});
+  Assert.equal(links.length, 2, "only have bookmarks");
+  Assert.equal(links[0].url, bookmarks[1].url, "still have younger bookmark");
+  Assert.equal(links[1].url, bookmarks[0].url, "still have older bookmark");
+
+  links = await provider.getHighlights({excludeBookmarks: true});
+  Assert.equal(links.length, 1, "only have history");
+  Assert.equal(links[0].url, testURI, "only have the history now");
+
+  links = await provider.getHighlights({excludeBookmarks: true, excludeHistory: true});
+  Assert.equal(links.length, 0, "requested nothing, get nothing");
+
+  links = await provider.getHighlights({bookmarkSecondsAgo: oldSeconds / 2});
+  Assert.equal(links.length, 2, "old bookmark filtered out with");
+  Assert.equal(links[0].url, bookmarks[1].url, "still have newer bookmark");
+  Assert.equal(links[1].url, testURI, "still have the history");
+
+  // Add a visit and metadata to the older bookmark
+  await PlacesTestUtils.addVisits(bookmarks[0].url);
+  await addMetadata(bookmarks[0].url);
+
+  links = await provider.getHighlights({bookmarkSecondsAgo: oldSeconds / 2});
+  Assert.equal(links.length, 3, "old bookmark returns as history");
+  Assert.equal(links[0].url, bookmarks[1].url, "still have newer bookmark");
+  Assert.equal(links[1].url, bookmarks[0].url, "old bookmark now is newer history");
+  Assert.equal(links[1].type, "history", "old bookmark now is history");
+  Assert.equal(links[2].url, testURI, "still have the history");
+
+  // Bookmark the history item
+  await PlacesUtils.bookmarks.insert({
+    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+    title: "now a bookmark",
+    url: testURI
+  });
+
+  links = await provider.getHighlights();
+  Assert.equal(links.length, 3, "a visited bookmark doesn't appear as bookmark and history");
+  Assert.equal(links[0].url, testURI, "history is now the first, i.e., most recent, bookmark");
+  Assert.equal(links[0].type, "bookmark", "was history now bookmark");
+  Assert.equal(links[1].url, bookmarks[1].url, "still have younger bookmark now second");
+  Assert.equal(links[2].url, bookmarks[0].url, "still have older bookmark now third");
+});
+
 add_task(async function getTopFrecentSites() {
   await setUpActivityStreamTest();
 
   let provider = NewTabUtils.activityStreamLinks;
   let links = await provider.getTopSites({topsiteFrecency: 100});
   Assert.equal(links.length, 0, "empty history yields empty links");
 
   // add a visit