Skip to content

Commit

Permalink
Store history
Browse files Browse the repository at this point in the history
  • Loading branch information
CrisBarreiro committed Apr 2, 2024
1 parent 627f283 commit 1aa07e2
Show file tree
Hide file tree
Showing 13 changed files with 505 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package com.duckduckgo.app.browser

import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.webkit.CookieManager
Expand Down Expand Up @@ -78,6 +77,7 @@ import org.junit.Test
import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
Expand Down Expand Up @@ -708,7 +708,7 @@ class BrowserWebViewClientTest {
fun whenPageFinishesBeforeStartingThenPixelIsNotFired() {
val mockWebView = getImmediatelyInvokedMockWebView()
testee.onPageFinished(mockWebView, EXAMPLE_URL)
verify(pageLoadedHandler, never()).invoke(any(), any(), any())
verify(pageLoadedHandler, never()).onPageLoaded(any(), any(), any(), any())
}

@Test
Expand All @@ -722,7 +722,7 @@ class BrowserWebViewClientTest {
testee.onPageFinished(mockWebView, EXAMPLE_URL)
val startArgumentCaptor = argumentCaptor<Long>()
val endArgumentCaptor = argumentCaptor<Long>()
verify(pageLoadedHandler).invoke(any(), startArgumentCaptor.capture(), endArgumentCaptor.capture())
verify(pageLoadedHandler).onPageLoaded(any(), eq(null), startArgumentCaptor.capture(), endArgumentCaptor.capture())
assertEquals(0L, startArgumentCaptor.firstValue)
assertEquals(10L, endArgumentCaptor.firstValue)
}
Expand All @@ -735,7 +735,7 @@ class BrowserWebViewClientTest {
whenever(mockWebView.settings).thenReturn(mock())
testee.onPageStarted(mockWebView, "about:blank", null)
testee.onPageFinished(mockWebView, "about:blank")
verify(pageLoadedHandler, never()).invoke(any(), any(), any())
verify(pageLoadedHandler, never()).onPageLoaded(any(), any(), any(), any())
}

@Test
Expand All @@ -744,7 +744,7 @@ class BrowserWebViewClientTest {
whenever(mockWebView.settings).thenReturn(mock())
testee.onPageStarted(mockWebView, EXAMPLE_URL, null)
testee.onPageFinished(mockWebView, EXAMPLE_URL)
verify(pageLoadedHandler, never()).invoke(any(), any(), any())
verify(pageLoadedHandler, never()).onPageLoaded(any(), any(), any(), any())
}

@Test
Expand All @@ -761,7 +761,7 @@ class BrowserWebViewClientTest {

val startArgumentCaptor = argumentCaptor<Long>()
val endArgumentCaptor = argumentCaptor<Long>()
verify(pageLoadedHandler).invoke(any(), startArgumentCaptor.capture(), endArgumentCaptor.capture())
verify(pageLoadedHandler).onPageLoaded(any(), eq(null), startArgumentCaptor.capture(), endArgumentCaptor.capture())
assertEquals(0L, startArgumentCaptor.firstValue)
assertEquals(10L, endArgumentCaptor.firstValue)
}
Expand All @@ -783,7 +783,7 @@ class BrowserWebViewClientTest {
testee.onPageFinished(mockWebView, EXAMPLE_URL)
val startArgumentCaptor = argumentCaptor<Long>()
val endArgumentCaptor = argumentCaptor<Long>()
verify(pageLoadedHandler).invoke(any(), startArgumentCaptor.capture(), endArgumentCaptor.capture())
verify(pageLoadedHandler).onPageLoaded(any(), eq(null), startArgumentCaptor.capture(), endArgumentCaptor.capture())
assertEquals(5L, startArgumentCaptor.firstValue)
assertEquals(10L, endArgumentCaptor.firstValue)
}
Expand Down Expand Up @@ -877,11 +877,6 @@ class BrowserWebViewClientTest {
private val fakeHistory: MutableList<WebHistoryItem> = mutableListOf()
private var fakeCurrentIndex = -1

fun addPageToHistory(webHistoryItem: WebHistoryItem) {
fakeHistory.add(webHistoryItem)
fakeCurrentIndex++
}

override fun getSize() = fakeHistory.size

override fun getItemAtIndex(index: Int): WebHistoryItem = fakeHistory[index]
Expand All @@ -893,21 +888,6 @@ class BrowserWebViewClientTest {
override fun clone(): WebBackForwardList = throw NotImplementedError()
}

private class TestHistoryItem(
private val url: String,
) : WebHistoryItem() {

override fun getUrl(): String = url

override fun getOriginalUrl(): String = url

override fun getTitle(): String = url

override fun getFavicon(): Bitmap = throw NotImplementedError()

override fun clone(): WebHistoryItem = throw NotImplementedError()
}

companion object {
const val EXAMPLE_URL = "example.com"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.duckduckgo.app.browser.pageloadpixel

import com.duckduckgo.app.history.SaveToHistory
import com.duckduckgo.app.pixels.remoteconfig.OptimizeTrackerEvaluationRCWrapper
import com.duckduckgo.autoconsent.api.Autoconsent
import com.duckduckgo.browser.api.WebViewVersionProvider
Expand Down Expand Up @@ -28,6 +29,7 @@ class PageLoadedHandlerTest {
private val webViewVersionProvider: WebViewVersionProvider = mock()
private val pageLoadedPixelDao: PageLoadedPixelDao = mock()
private val autoconsent: Autoconsent = mock()
private val saveToHistory: SaveToHistory = mock()

private val testee = RealPageLoadedHandler(
deviceInfo,
Expand All @@ -40,6 +42,7 @@ class PageLoadedHandlerTest {
override val enabled: Boolean
get() = true
},
saveToHistory,
)

@Before
Expand All @@ -51,15 +54,15 @@ class PageLoadedHandlerTest {

@Test
fun whenInvokingWithValidUrlThenPixelIsAdded() {
testee.invoke(VALID_URL, 0L, 10L)
testee.onPageLoaded(VALID_URL, "title", 0L, 10L)
val argumentCaptor = argumentCaptor<PageLoadedPixelEntity>()
verify(pageLoadedPixelDao).add(argumentCaptor.capture())
Assert.assertEquals(10L, argumentCaptor.firstValue.elapsedTime)
}

@Test
fun whenInvokingWithInvalidUrlThenPixelIsAdded() {
testee.invoke(INVALID_URL, 0L, 10L)
testee.onPageLoaded(INVALID_URL, "title", 0L, 10L)
verify(pageLoadedPixelDao, never()).add(any())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.history

import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.duckduckgo.app.history.store.HistoryDao
import com.duckduckgo.app.history.store.HistoryDatabase
import com.duckduckgo.app.history.store.HistoryEntryEntity
import com.duckduckgo.app.history.store.HistoryEntryWithVisits
import io.reactivex.observers.TestObserver
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class HistoryDaoTest {

private lateinit var historyDao: HistoryDao
private lateinit var db: HistoryDatabase

@Before
fun setup() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
db = Room.inMemoryDatabaseBuilder(context, HistoryDatabase::class.java).build()
historyDao = db.historyDao()
}

@After
fun tearDown() {
db.close()
}

@Test
fun testGetHistoryEntryByUrl() {
val historyEntry = HistoryEntryEntity(url = "url", title = "title", query = "query", isSerp = false)
historyDao.insertHistoryEntry(historyEntry)

val retrievedEntry = historyDao.getHistoryEntryByUrl("url")
Assert.assertNotNull(retrievedEntry)
Assert.assertEquals(historyEntry.url, retrievedEntry?.url)
}

@Test
fun whenInsertSameUrlWithSameDateTwiceThenOnlyOneEntryAndOneVisitAreStored() {
historyDao.updateOrInsertVisit("url", "title", "query", false, 1L)
historyDao.updateOrInsertVisit("url", "title", "query", false, 1L)

val historyEntriesWithVisits = historyDao.getHistoryEntriesWithVisits()
val testObserver = TestObserver<List<HistoryEntryWithVisits>>()
historyEntriesWithVisits.subscribe(testObserver)
Assert.assertEquals(1, testObserver.valueCount())
Assert.assertEquals(1, testObserver.values().first().first().visits.count())
}

@Test
fun whenInsertSameUrlWithDifferentDateTwiceThenOneEntryAndTwoVisitsAreStored() {
historyDao.updateOrInsertVisit("url", "title", "query", false, 1L)
historyDao.updateOrInsertVisit("url", "title", "query", false, 2L)

val historyEntriesWithVisits = historyDao.getHistoryEntriesWithVisits()
val testObserver = TestObserver<List<HistoryEntryWithVisits>>()
historyEntriesWithVisits.subscribe(testObserver)
Assert.assertEquals(1, testObserver.valueCount())
Assert.assertEquals(2, testObserver.values().first().first().visits.count())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class BrowserWebViewClient @Inject constructor(
private val crashLogger: CrashLogger,
private val jsPlugins: PluginPoint<JsInjectorPlugin>,
private val currentTimeProvider: CurrentTimeProvider,
private val shouldSendPageLoadedPixel: PageLoadedHandler,
private val pageLoadedHandler: PageLoadedHandler,
private val shouldSendPagePaintedPixel: PagePaintedHandler,
private val mediaPlayback: MediaPlayback,
) : WebViewClient() {
Expand Down Expand Up @@ -317,6 +317,7 @@ class BrowserWebViewClient @Inject constructor(
url: String?,
) {
Timber.v("onPageFinished webViewUrl: ${webView.url} URL: $url progress: ${webView.progress}")
// See https://app.asana.com/0/0/1206159443951489/f (WebView limitations)
if (webView.progress == 100) {
jsPlugins.getPlugins().forEach {
it.onPageFinished(webView, url, webViewClientListener?.getSite())
Expand All @@ -334,11 +335,9 @@ class BrowserWebViewClient @Inject constructor(
printInjector.injectPrint(webView)

url?.let {
start?.let { safeStart ->
val progress = webView.progress
// See https://app.asana.com/0/0/1206159443951489/f (WebView limitations)
if (url != ABOUT_BLANK) {
shouldSendPageLoadedPixel(it, safeStart, currentTimeProvider.getTimeInMillis())
if (url != ABOUT_BLANK) {
start?.let { safeStart ->
pageLoadedHandler.onPageLoaded(it, navigationList.currentItem?.title, safeStart, currentTimeProvider.getTimeInMillis())
shouldSendPagePaintedPixel(webView = webView, url = it)
start = null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.duckduckgo.app.browser.pageloadpixel
import com.duckduckgo.app.browser.UriString
import com.duckduckgo.app.browser.pageloadpixel.PageLoadedSites.Companion.sites
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.history.SaveToHistory
import com.duckduckgo.app.pixels.remoteconfig.OptimizeTrackerEvaluationRCWrapper
import com.duckduckgo.autoconsent.api.Autoconsent
import com.duckduckgo.browser.api.WebViewVersionProvider
Expand All @@ -31,7 +32,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

interface PageLoadedHandler {
operator fun invoke(url: String, start: Long, end: Long)
fun onPageLoaded(url: String, title: String?, start: Long, end: Long)
}

@ContributesBinding(AppScope::class)
Expand All @@ -43,9 +44,10 @@ class RealPageLoadedHandler @Inject constructor(
private val dispatcherProvider: DispatcherProvider,
private val autoconsent: Autoconsent,
private val optimizeTrackerEvaluationRCWrapper: OptimizeTrackerEvaluationRCWrapper,
private val saveToHistory: SaveToHistory,
) : PageLoadedHandler {

override operator fun invoke(url: String, start: Long, end: Long) {
override fun onPageLoaded(url: String, title: String?, start: Long, end: Long) {
appCoroutineScope.launch(dispatcherProvider.io()) {
if (sites.any { UriString.sameOrSubdomain(url, it) }) {
pageLoadedPixelDao.add(
Expand All @@ -58,6 +60,7 @@ class RealPageLoadedHandler @Inject constructor(
),
)
}
saveToHistory.saveToHistory(url, title)
}
}
}
52 changes: 52 additions & 0 deletions app/src/main/java/com/duckduckgo/app/history/HistoryEntry.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.history

import android.net.Uri
import androidx.core.net.toUri
import com.duckduckgo.app.history.HistoryEntry.VisitedPage
import com.duckduckgo.app.history.HistoryEntry.VisitedSERP
import com.duckduckgo.app.history.store.HistoryEntryWithVisits
import java.util.Date

sealed class HistoryEntry(
open val url: Uri,
open val title: String,
open val visits: List<Date>,
) {

data class VisitedPage(
override val url: Uri,
override val title: String,
override val visits: List<Date>,
) : HistoryEntry(url = url, title = title, visits = visits)

data class VisitedSERP(
override val url: Uri,
override val title: String,
val query: String,
val queryTokens: List<String>? = null,
override val visits: List<Date>,
) : HistoryEntry(url = url, title, visits = visits)
}

fun HistoryEntryWithVisits.toHistoryEntry(): HistoryEntry {
return when (historyEntry.isSerp) {
true -> VisitedSERP(historyEntry.url.toUri(), historyEntry.title, historyEntry.query ?: "", visits = visits.map { Date(it.date) })
false -> VisitedPage(historyEntry.url.toUri(), historyEntry.title, visits.map { Date(it.date) })
}
}

0 comments on commit 1aa07e2

Please sign in to comment.