Skip to content

Commit

Permalink
For mozilla-mobile#20893 - Search term groups in history
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielluong committed Sep 2, 2021
1 parent bca1775 commit 749e3f4
Show file tree
Hide file tree
Showing 22 changed files with 862 additions and 55 deletions.
1 change: 1 addition & 0 deletions app/src/main/java/org/mozilla/fenix/BrowserDirection.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromSettings(R.id.settingsFragment),
FromBookmarks(R.id.bookmarkFragment),
FromHistory(R.id.historyFragment),
FromHistoryMetadataGroup(R.id.historyMetadataGroupFragment),
FromTrackingProtectionExceptions(R.id.trackingProtectionExceptionsFragment),
FromAbout(R.id.aboutFragment),
FromTrackingProtection(R.id.trackingProtectionFragment),
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/org/mozilla/fenix/FeatureFlags.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,9 @@ object FeatureFlags {
* Enables showing the home screen behind the search dialog
*/
val showHomeBehindSearch = Config.channel.isNightlyOrDebug

/**
* Enables showing search groupings in the History.
*/
val showHistorySearchGroups = Config.channel.isDebug
}
9 changes: 6 additions & 3 deletions app/src/main/java/org/mozilla/fenix/HomeActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import android.os.StrictMode
import android.os.SystemClock
import android.text.format.DateUtils
import android.util.AttributeSet
import android.view.ActionMode
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ActionMode
import android.view.ViewConfiguration
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.annotation.CallSuper
Expand All @@ -34,12 +34,12 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers.IO
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.search.SearchEngine
Expand Down Expand Up @@ -93,6 +93,7 @@ import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.bookmarks.DesktopFolders
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker
import org.mozilla.fenix.perf.Performance
Expand Down Expand Up @@ -740,6 +741,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistory ->
HistoryFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistoryMetadataGroup ->
HistoryMetadataGroupFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtectionExceptions ->
TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAbout ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@

package org.mozilla.fenix.components.history

import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import mozilla.components.concept.storage.HistoryMetadata
import mozilla.components.concept.storage.VisitInfo
import mozilla.components.concept.storage.VisitType
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.library.history.HistoryItem
import org.mozilla.fenix.library.history.HistoryItemType
import org.mozilla.fenix.perf.runBlockingIncrement

/**
Expand All @@ -19,36 +24,138 @@ interface PagedHistoryProvider {
* @param numberOfItems How many items to fetch
* @param onComplete A callback that returns the list of [VisitInfo]
*/
fun getHistory(offset: Long, numberOfItems: Long, onComplete: (List<VisitInfo>) -> Unit)
fun getHistory(offset: Long, numberOfItems: Long, onComplete: (List<HistoryItem>) -> Unit)
}

// A PagedList DataSource runs on a background thread automatically.
// If we run this in our own coroutineScope it breaks the PagedList
fun HistoryStorage.createSynchronousPagedHistoryProvider(): PagedHistoryProvider {
return object : PagedHistoryProvider {

override fun getHistory(
offset: Long,
numberOfItems: Long,
onComplete: (List<VisitInfo>) -> Unit
) {
runBlockingIncrement {
val history = getVisitsPaginated(
offset,
numberOfItems,
excludeTypes = listOf(
VisitType.NOT_A_VISIT,
VisitType.DOWNLOAD,
VisitType.REDIRECT_TEMPORARY,
VisitType.RELOAD,
VisitType.EMBED,
VisitType.FRAMED_LINK,
VisitType.REDIRECT_PERMANENT
/**
* @param historyStorage
*/
class DefaultPagedHistoryProvider(
private val historyStorage: PlacesHistoryStorage
) : PagedHistoryProvider {

@Suppress("LongMethod")
override fun getHistory(
offset: Long,
numberOfItems: Long,
onComplete: (List<HistoryItem>) -> Unit
) {
// A PagedList DataSource runs on a background thread automatically.
// If we run this in our own coroutineScope it breaks the PagedList
runBlockingIncrement {
if (FeatureFlags.showHistorySearchGroups) {
val historyMetadata: MutableList<HistoryMetadata> =
historyStorage.getHistoryMetadataSince(Long.MIN_VALUE)
.filter { it.totalViewTime > 0 && it.key.searchTerm != null }
.toMutableList()
val historyGroups: MutableMap<String, List<HistoryMetadata>> = historyMetadata
.groupBy { it.key.searchTerm!! }
.toMutableMap()

val history: List<HistoryItem> = historyStorage
.getVisitsPaginated(
offset,
numberOfItems,
excludeTypes = listOf(
VisitType.NOT_A_VISIT,
VisitType.DOWNLOAD,
VisitType.REDIRECT_TEMPORARY,
VisitType.RELOAD,
VisitType.EMBED,
VisitType.FRAMED_LINK,
VisitType.REDIRECT_PERMANENT
)
)
)
.mapIndexed(transformVisitInfoToHistoryItem(offset.toInt()))

val result = mutableListOf<HistoryItem>()

// Iterate through the existing list of visited history item and filter out items
// that belong to a search group, replacing the first seen item that matches a
// history metadata item with its corresponding search group.
for (item in history) {
val matchingHistoryMetadataItem =
historyMetadata.find { it.key.url == item.url }

if (matchingHistoryMetadataItem != null) {
// Found a visited history item matching an existing history metadata item

val searchTerm = matchingHistoryMetadataItem.key.searchTerm!!
val historyMetadataItems = historyGroups[searchTerm]

historyMetadata.remove(matchingHistoryMetadataItem)

// Replace the visited history item with a history group along with all the
// matching history metadata item with the same search term.
if (historyMetadataItems != null && historyMetadataItems.isNotEmpty()) {
result.add(
HistoryItem(
id = item.id,
title = searchTerm,
visitedAt = item.visitedAt,
type = HistoryItemType.GROUP,
historyMetadataItems = historyMetadataItems.map {
HistoryItem(
id = 0,
title = it.title?.takeIf(String::isNotEmpty)
?: it.key.url.tryGetHostFromUrl(),
url = it.key.url,
visitedAt = it.updatedAt,
type = HistoryItemType.HISTORY_METADATA_ITEM
)
}
)
)

// Since a history group was added for the existing `searchTerm`,
// remove the `searchTerm` from `historyGroups`. Subsequent matches
// of the visited history item and an existing history metadata with
// a search group that has already been added will not be appended
// to the `result`.
historyGroups.remove(searchTerm)
}
} else {
// Visited history item does not belong in a search group. Add to `result`.
result.add(item)
}
}

onComplete(result)
} else {
val history = historyStorage
.getVisitsPaginated(
offset,
numberOfItems,
excludeTypes = listOf(
VisitType.NOT_A_VISIT,
VisitType.DOWNLOAD,
VisitType.REDIRECT_TEMPORARY,
VisitType.RELOAD,
VisitType.EMBED,
VisitType.FRAMED_LINK,
VisitType.REDIRECT_PERMANENT
)
)
.mapIndexed(transformVisitInfoToHistoryItem(offset.toInt()))

onComplete(history)
}
}
}

private fun transformVisitInfoToHistoryItem(offset: Int): (id: Int, visit: VisitInfo) -> HistoryItem {
return { id, visit ->
val title = visit.title
?.takeIf(String::isNotEmpty)
?: visit.url.tryGetHostFromUrl()

HistoryItem(
id = offset + id,
title = title,
url = visit.url,
visitedAt = visit.visitTime,
type = HistoryItemType.ITEM
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.utils.Do

@Suppress("TooManyFunctions")
interface HistoryController {
Expand Down Expand Up @@ -50,8 +51,21 @@ class DefaultHistoryController(
private val syncHistory: suspend () -> Unit,
private val metrics: MetricController
) : HistoryController {

override fun handleOpen(item: HistoryItem) {
openToBrowser(item)
Do exhaustive when (item.type) {
HistoryItemType.HISTORY_METADATA_ITEM,
HistoryItemType.ITEM -> openToBrowser(item)
HistoryItemType.GROUP -> {
navController.navigate(
HistoryFragmentDirections.actionGlobalHistoryMetadataGroup(
title = item.title,
historyMetadataItems = item.historyMetadataItems.toTypedArray()
),
NavOptions.Builder().setPopUpTo(R.id.historyMetadataGroupFragment, true).build()
)
}
}
}

override fun handleOpenInNewTab(item: HistoryItem, mode: BrowsingMode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
package org.mozilla.fenix.library.history

import androidx.paging.ItemKeyedDataSource
import mozilla.components.concept.storage.VisitInfo
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
import org.mozilla.fenix.components.history.PagedHistoryProvider

class HistoryDataSource(
private val historyProvider: PagedHistoryProvider
) : ItemKeyedDataSource<Int, HistoryItem>() {

// Because the pagination is not based off of they key
// Because the pagination is not based off of the key
// we want to start at 1, not 0 to be able to send the correct offset
// to the `historyProvider.getHistory` call.
override fun getKey(item: HistoryItem): Int = item.id + 1
Expand All @@ -23,31 +21,19 @@ class HistoryDataSource(
callback: LoadInitialCallback<HistoryItem>
) {
historyProvider.getHistory(INITIAL_OFFSET, params.requestedLoadSize.toLong()) { history ->
val items = history.mapIndexed(transformVisitInfoToHistoryItem(INITIAL_OFFSET.toInt()))
callback.onResult(items)
callback.onResult(history)
}
}

override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<HistoryItem>) {
historyProvider.getHistory(params.key.toLong(), params.requestedLoadSize.toLong()) { history ->
val items = history.mapIndexed(transformVisitInfoToHistoryItem(params.key))
callback.onResult(items)
callback.onResult(history)
}
}

override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<HistoryItem>) { /* noop */ }

companion object {
private const val INITIAL_OFFSET = 0L

fun transformVisitInfoToHistoryItem(offset: Int): (id: Int, visit: VisitInfo) -> HistoryItem {
return { id, visit ->
val title = visit.title
?.takeIf(String::isNotEmpty)
?: visit.url.tryGetHostFromUrl()

HistoryItem(offset + id, title, visit.url, visit.visitTime)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import org.mozilla.fenix.addons.showSnackBar
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.history.createSynchronousPagedHistoryProvider
import org.mozilla.fenix.components.history.DefaultPagedHistoryProvider
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.databinding.FragmentHistoryBinding
import org.mozilla.fenix.ext.components
Expand Down Expand Up @@ -122,7 +122,7 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
super.onCreate(savedInstanceState)

viewModel = HistoryViewModel(
requireComponents.core.historyStorage.createSynchronousPagedHistoryProvider()
historyProvider = DefaultPagedHistoryProvider(requireComponents.core.historyStorage)
)

viewModel.userHasHistory.observe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,53 @@

package org.mozilla.fenix.library.history

import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store

/**
* Class representing a history entry
*
* @property id Unique id of the history item
* @property title Title of the history item
* @property url URL of the history item
* @property visitedAt Timestamp of when this history item was visited
* @property type
* @property historyMetadataItems
* @property selected
*/
data class HistoryItem(val id: Int, val title: String, val url: String, val visitedAt: Long)
@Parcelize
data class HistoryItem(
val id: Int,
val title: String,
val url: String = "",
val visitedAt: Long,
val type: HistoryItemType = HistoryItemType.ITEM,
val historyMetadataItems: List<HistoryItem> = emptyList(),
val selected: Boolean = false,
) : Parcelable

/**
* Enum of the types of items that are displayed in the history view.
*/
enum class HistoryItemType {
/**
* A visited history item.
*/
ITEM,

/**
* A history metadata item.
*/
HISTORY_METADATA_ITEM,

/**
* A search group.
*/
GROUP
}

/**
* The [Store] for holding the [HistoryFragmentState] and applying [HistoryFragmentAction]s.
Expand Down
Loading

0 comments on commit 749e3f4

Please sign in to comment.