Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
The Tor Project
Applications
android-components
Commits
c78e6a71
Commit
c78e6a71
authored
Aug 21, 2020
by
Gabriel Luong
Browse files
Issue #7978: Part 4 - Implement TopSitesFeature
parent
dc7ee797
Changes
19
Hide whitespace changes
Inline
Side-by-side
components/feature/top-sites/build.gradle
View file @
c78e6a71
...
...
@@ -43,7 +43,14 @@ android {
}
}
tasks
.
withType
(
org
.
jetbrains
.
kotlin
.
gradle
.
tasks
.
KotlinCompile
).
all
{
kotlinOptions
.
freeCompilerArgs
+=
[
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi"
]
}
dependencies
{
implementation
project
(
':browser-storage-sync'
)
implementation
project
(
':support-ktx'
)
implementation
project
(
':support-base'
)
...
...
@@ -59,8 +66,10 @@ dependencies {
testImplementation
project
(
':support-test'
)
testImplementation
Dependencies
.
androidx_test_core
testImplementation
Dependencies
.
androidx_test_junit
testImplementation
Dependencies
.
testing_junit
testImplementation
Dependencies
.
testing_mockito
testImplementation
Dependencies
.
testing_coroutines
testImplementation
Dependencies
.
testing_robolectric
testImplementation
Dependencies
.
kotlin_coroutines
...
...
components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/DefaultTopSitesStorage.kt
0 → 100644
View file @
c78e6a71
/* 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/. */
package
mozilla.components.feature.top.sites
import
kotlinx.coroutines.CoroutineScope
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.launch
import
mozilla.components.browser.storage.sync.PlacesHistoryStorage
import
mozilla.components.feature.top.sites.ext.toTopSite
import
mozilla.components.feature.top.sites.TopSite.Type.FRECENT
import
mozilla.components.feature.top.sites.ext.hasUrl
import
mozilla.components.support.base.observer.Observable
import
mozilla.components.support.base.observer.ObserverRegistry
import
kotlin.coroutines.CoroutineContext
/**
* Default implementation of [TopSitesStorage].
*
* @param pinnedSitesStorage An instance of [PinnedSiteStorage], used for storing pinned sites.
* @param historyStorage An instance of [PlacesHistoryStorage], used for retrieving top frecent
* sites from history.
* @param defaultTopSites A list containing a title to url pair of default top sites to be added
* to the [PinnedSiteStorage].
*/
class
DefaultTopSitesStorage
(
private
val
pinnedSitesStorage
:
PinnedSiteStorage
,
private
val
historyStorage
:
PlacesHistoryStorage
,
private
val
defaultTopSites
:
List
<
Pair
<
String
,
String
>>
=
listOf
(),
coroutineContext
:
CoroutineContext
=
Dispatchers
.
IO
)
:
TopSitesStorage
,
Observable
<
TopSitesStorage
.
Observer
>
by
ObserverRegistry
()
{
private
var
scope
=
CoroutineScope
(
coroutineContext
)
// Cache of the last retrieved top sites
var
cachedTopSites
=
listOf
<
TopSite
>()
init
{
if
(
defaultTopSites
.
isNotEmpty
())
{
scope
.
launch
{
defaultTopSites
.
forEach
{
(
title
,
url
)
->
addPinnedSite
(
title
,
url
,
isDefault
=
true
)
}
}
}
}
override
fun
addPinnedSite
(
title
:
String
,
url
:
String
,
isDefault
:
Boolean
)
{
scope
.
launch
{
pinnedSitesStorage
.
addPinnedSite
(
title
,
url
,
isDefault
)
notifyObservers
{
onStorageUpdated
()
}
}
}
override
fun
removeTopSite
(
topSite
:
TopSite
)
{
scope
.
launch
{
if
(
topSite
.
type
==
FRECENT
)
{
historyStorage
.
deleteVisitsFor
(
topSite
.
url
)
notifyObservers
{
onStorageUpdated
()
}
}
else
{
pinnedSitesStorage
.
removePinnedSite
(
topSite
)
notifyObservers
{
onStorageUpdated
()
}
}
}
}
override
suspend
fun
getTopSites
(
totalSites
:
Int
,
includeFrecent
:
Boolean
):
List
<
TopSite
>
{
val
topSites
=
ArrayList
<
TopSite
>()
val
pinnedSites
=
pinnedSitesStorage
.
getPinnedSites
().
take
(
totalSites
)
val
numSitesRequired
=
totalSites
-
pinnedSites
.
size
topSites
.
addAll
(
pinnedSites
)
if
(
includeFrecent
&&
numSitesRequired
>
0
)
{
// Get twice the required size to buffer for duplicate entries with
// existing pinned sites
val
frecentSites
=
historyStorage
.
getTopFrecentSites
(
numSitesRequired
*
2
)
.
map
{
it
.
toTopSite
()
}
.
filter
{
!
pinnedSites
.
hasUrl
(
it
.
url
)
}
.
take
(
numSitesRequired
)
topSites
.
addAll
(
frecentSites
)
}
cachedTopSites
=
topSites
return
topSites
}
}
components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/PinnedSiteStorage.kt
View file @
c78e6a71
...
...
@@ -5,62 +5,51 @@
package
mozilla.components.feature.top.sites
import
android.content.Context
import
androidx.paging.DataSource
import
kotlinx.coroutines.flow.Flow
import
kotlinx.coroutines.flow.map
import
mozilla.components.feature.top.sites.adapter.PinnedSiteAdapter
import
kotlinx.coroutines.Dispatchers.IO
import
kotlinx.coroutines.withContext
import
mozilla.components.feature.top.sites.db.TopSiteDatabase
import
mozilla.components.feature.top.sites.db.PinnedSiteEntity
import
mozilla.components.feature.top.sites.db.toPinnedSite
/**
* A storage implementation for organizing pinned sites.
*/
class
PinnedSiteStorage
(
context
:
Context
)
{
class
PinnedSiteStorage
(
context
:
Context
)
{
internal
var
database
:
Lazy
<
TopSiteDatabase
>
=
lazy
{
TopSiteDatabase
.
get
(
context
)
}
private
val
pinnedSiteDao
by
lazy
{
database
.
value
.
pinnedSiteDao
()
}
/**
* Adds a new
[P
inned
S
ite
]
.
* Adds a new
p
inned
s
ite.
*
* @param title The title string.
* @param url The URL string.
* @param isDefault Whether or not the pinned site added should be a default pinned site. This
* is used to identify pinned sites that are added by the application.
*/
fun
addPinnedSite
(
title
:
String
,
url
:
String
,
isDefault
:
Boolean
=
false
)
{
PinnedSiteEntity
(
suspend
fun
addPinnedSite
(
title
:
String
,
url
:
String
,
isDefault
:
Boolean
=
false
)
=
withContext
(
IO
)
{
val
entity
=
PinnedSiteEntity
(
title
=
title
,
url
=
url
,
isDefault
=
isDefault
,
createdAt
=
System
.
currentTimeMillis
()
).
also
{
entity
->
entity
.
id
=
database
.
value
.
pinnedSiteDao
().
insertPinnedSite
(
entity
)
}
)
entity
.
id
=
pinnedSiteDao
.
insertPinnedSite
(
entity
)
}
/**
* Returns a
[Flow]
list of all the
[P
inned
Site] instanc
es.
* Returns a list of all the
p
inned
sit
es.
*/
fun
getPinnedSites
():
Flow
<
List
<
PinnedSite
>>
{
return
database
.
value
.
pinnedSiteDao
().
getPinnedSites
().
map
{
list
->
list
.
map
{
entity
->
PinnedSiteAdapter
(
entity
)
}
}
suspend
fun
getPinnedSites
():
List
<
TopSite
>
=
withContext
(
IO
)
{
pinnedSiteDao
.
getPinnedSites
().
map
{
entity
->
entity
.
toTopSite
()
}
}
/**
* Returns all [PinnedSite]s as a [DataSource.Factory].
*/
fun
getPinnedSitesPaged
():
DataSource
.
Factory
<
Int
,
PinnedSite
>
=
database
.
value
.
pinnedSiteDao
()
.
getPinnedSitesPaged
()
.
map
{
entity
->
PinnedSiteAdapter
(
entity
)
}
/**
* Removes the given [PinnedSite].
* Removes the given pinned site.
*
* @param site The pinned site.
*/
fun
removePinnedSite
(
site
:
PinnedSite
)
{
val
pinnedSiteEntity
=
(
site
as
PinnedSiteAdapter
).
entity
database
.
value
.
pinnedSiteDao
().
deletePinnedSite
(
pinnedSiteEntity
)
suspend
fun
removePinnedSite
(
site
:
TopSite
)
=
withContext
(
IO
)
{
pinnedSiteDao
.
deletePinnedSite
(
site
.
toPinnedSite
())
}
}
components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSite.kt
0 → 100644
View file @
c78e6a71
/* 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/. */
package
mozilla.components.feature.top.sites
/**
* A top site.
*
* @property id Unique ID of this top site.
* @property title The title of the top site.
* @property url The URL of the top site.
* @property createdAt The optional date the top site was added.
* @property type The type of a top site.
*/
data class
TopSite
(
val
id
:
Long
?,
val
title
:
String
?,
val
url
:
String
,
val
createdAt
:
Long
?,
val
type
:
Type
)
{
/**
* The type of a [TopSite].
*/
enum
class
Type
{
/**
* This top site was added as a default by the application.
*/
DEFAULT
,
/**
* This top site was pinned by an user.
*/
PINNED
,
/**
* This top site is auto-generated from the history storage based on the most frecent site.
*/
FRECENT
}
}
components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/
PinnedSite
.kt
→
components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/
TopSitesConfig
.kt
View file @
c78e6a71
...
...
@@ -5,26 +5,13 @@
package
mozilla.components.feature.top.sites
/**
* A pinned site.
* Top sites configuration to specify the number of top sites to display and
* whether or not to include top frecent sites in the top sites feature.
*
* @property totalSites A total number of sites that will be displayed.
* @property includeFrecent If true, includes frecent top site results.
*/
interface
PinnedSite
{
/**
* Unique ID of this pinned site.
*/
val
id
:
Long
/**
* The title of the pinned site.
*/
val
title
:
String
/**
* The URL of the pinned site.
*/
val
url
:
String
/**
* Whether or not the pinned site is a default pinned site (added as a default by the application).
*/
val
isDefault
:
Boolean
}
data class
TopSitesConfig
(
val
totalSites
:
Int
,
val
includeFrecent
:
Boolean
)
components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesFeature.kt
0 → 100644
View file @
c78e6a71
/* 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/. */
package
mozilla.components.feature.top.sites
import
mozilla.components.feature.top.sites.presenter.DefaultTopSitesPresenter
import
mozilla.components.feature.top.sites.presenter.TopSitesPresenter
import
mozilla.components.feature.top.sites.view.TopSitesView
import
mozilla.components.support.base.feature.LifecycleAwareFeature
/**
* View-bound feature that updates the UI when the [TopSitesStorage] is updated.
*
* @param view An implementor of [TopSitesView] that will be notified of changes to the storage.
* @param storage The top sites storage that stores pinned and frecent sites.
* @param config Lambda expression that returns [TopSitesConfig] which species the number of top
* sites to return and whether or not to include frequently visited sites.
*/
class
TopSitesFeature
(
private
val
view
:
TopSitesView
,
val
storage
:
TopSitesStorage
,
val
config
:
()
->
TopSitesConfig
,
private
val
presenter
:
TopSitesPresenter
=
DefaultTopSitesPresenter
(
view
,
storage
,
config
)
)
:
LifecycleAwareFeature
{
override
fun
start
()
{
presenter
.
start
()
}
override
fun
stop
()
{
presenter
.
stop
()
}
}
components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesStorage.kt
0 → 100644
View file @
c78e6a71
/* 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/. */
package
mozilla.components.feature.top.sites
import
mozilla.components.support.base.observer.Observable
/**
* Abstraction layer above the [PinnedSiteStorage] and [PlacesHistoryStorage] storages.
*/
interface
TopSitesStorage
:
Observable
<
TopSitesStorage
.
Observer
>
{
/**
* Adds a new pinned site.
*
* @param title The title string.
* @param url The URL string.
* @param isDefault Whether or not the pinned site added should be a default pinned site. This
* is used to identify pinned sites that are added by the application.
*/
fun
addPinnedSite
(
title
:
String
,
url
:
String
,
isDefault
:
Boolean
=
false
)
/**
* Removes the given [TopSite].
*
* @param topSite The top site.
*/
fun
removeTopSite
(
topSite
:
TopSite
)
/**
* Return a unified list of top sites based on the given number of sites desired.
* If `includeFrecent` is true, fill in any missing top sites with frecent top site results.
*
* @param totalSites A total number of sites that will be retrieve if possible.
* @param includeFrecent If true, includes frecent top site results.
*/
suspend
fun
getTopSites
(
totalSites
:
Int
,
includeFrecent
:
Boolean
):
List
<
TopSite
>
/**
* Interface to be implemented by classes that want to observe the top site storage.
*/
interface
Observer
{
/**
* Notify the observer when changes are made to the storage.
*/
fun
onStorageUpdated
()
}
}
components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/TopSitesUseCases.kt
0 → 100644
View file @
c78e6a71
/* 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/. */
package
mozilla.components.feature.top.sites
/**
* Contains use cases related to the top sites feature.
*/
class
TopSitesUseCases
(
topSitesStorage
:
TopSitesStorage
)
{
/**
* Add a pinned site use case.
*/
class
AddPinnedSiteUseCase
internal
constructor
(
private
val
storage
:
TopSitesStorage
)
{
/**
* Adds a new [PinnedSite].
*
* @param title The title string.
* @param url The URL string.
*/
operator
fun
invoke
(
title
:
String
,
url
:
String
,
isDefault
:
Boolean
=
false
)
{
storage
.
addPinnedSite
(
title
,
url
,
isDefault
)
}
}
/**
* Remove a top site use case.
*/
class
RemoveTopSiteUseCase
internal
constructor
(
private
val
storage
:
TopSitesStorage
)
{
/**
* Removes the given [TopSite].
*
* @param topSite The top site.
*/
operator
fun
invoke
(
topSite
:
TopSite
)
{
storage
.
removeTopSite
(
topSite
)
}
}
val
addPinnedSites
:
AddPinnedSiteUseCase
by
lazy
{
AddPinnedSiteUseCase
(
topSitesStorage
)
}
val
removeTopSites
:
RemoveTopSiteUseCase
by
lazy
{
RemoveTopSiteUseCase
(
topSitesStorage
)
}
}
components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/adapter/PinnedSiteAdapter.kt
deleted
100644 → 0
View file @
dc7ee797
/* 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/. */
package
mozilla.components.feature.top.sites.adapter
import
mozilla.components.feature.top.sites.PinnedSite
import
mozilla.components.feature.top.sites.db.PinnedSiteEntity
internal
class
PinnedSiteAdapter
(
internal
val
entity
:
PinnedSiteEntity
)
:
PinnedSite
{
override
val
id
:
Long
get
()
=
entity
.
id
!!
override
val
title
:
String
get
()
=
entity
.
title
override
val
url
:
String
get
()
=
entity
.
url
override
val
isDefault
:
Boolean
get
()
=
entity
.
isDefault
override
fun
equals
(
other
:
Any
?):
Boolean
{
if
(
other
!
is
PinnedSiteAdapter
)
{
return
false
}
return
entity
==
other
.
entity
}
override
fun
hashCode
():
Int
{
return
entity
.
hashCode
()
}
}
components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/PinnedSiteDao.kt
View file @
c78e6a71
...
...
@@ -4,30 +4,26 @@
package
mozilla.components.feature.top.sites.db
import
androidx.
paging.DataSource
import
androidx.
annotation.WorkerThread
import
androidx.room.Dao
import
androidx.room.Delete
import
androidx.room.Insert
import
androidx.room.Query
import
androidx.room.Transaction
import
kotlinx.coroutines.flow.Flow
/**
* Internal DAO for accessing [PinnedSiteEntity] instances.
*/
@Dao
internal
interface
PinnedSiteDao
{
@WorkerThread
@Insert
fun
insertPinnedSite
(
site
:
PinnedSiteEntity
):
Long
@WorkerThread
@Delete
fun
deletePinnedSite
(
site
:
PinnedSiteEntity
)
@
Transaction
@
WorkerThread
@Query
(
"SELECT * FROM top_sites"
)
fun
getPinnedSites
():
Flow
<
List
<
PinnedSiteEntity
>>
@Transaction
@Query
(
"SELECT * FROM top_sites"
)
fun
getPinnedSitesPaged
():
DataSource
.
Factory
<
Int
,
PinnedSiteEntity
>
fun
getPinnedSites
():
List
<
PinnedSiteEntity
>
}
components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/db/PinnedSiteEntity.kt
View file @
c78e6a71
...
...
@@ -7,6 +7,9 @@ package mozilla.components.feature.top.sites.db
import
androidx.room.ColumnInfo
import
androidx.room.Entity
import
androidx.room.PrimaryKey
import
mozilla.components.feature.top.sites.TopSite
import
mozilla.components.feature.top.sites.TopSite.Type.DEFAULT
import
mozilla.components.feature.top.sites.TopSite.Type.PINNED
/**
* Internal entity representing a pinned site.
...
...
@@ -28,4 +31,25 @@ internal data class PinnedSiteEntity(
@ColumnInfo
(
name
=
"created_at"
)
var
createdAt
:
Long
=
System
.
currentTimeMillis
()
)
)
{
internal
fun
toTopSite
():
TopSite
{
val
type
=
if
(
isDefault
)
DEFAULT
else
PINNED
return
TopSite
(
id
,
title
,
url
,
createdAt
,
type
)
}
}
internal
fun
TopSite
.
toPinnedSite
():
PinnedSiteEntity
{
return
PinnedSiteEntity
(
id
=
id
,
title
=
title
?:
""
,
url
=
url
,
isDefault
=
type
===
DEFAULT
,
createdAt
=
createdAt
!!
)
}
components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/ext/TopFrecentSiteInfo.kt
0 → 100644
View file @
c78e6a71
/* 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/. */
package
mozilla.components.feature.top.sites.ext
import
mozilla.components.concept.storage.TopFrecentSiteInfo
import
mozilla.components.feature.top.sites.TopSite
import
mozilla.components.feature.top.sites.TopSite.Type.FRECENT
import
mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
/**
* Returns a [TopSite] for the given [TopFrecentSiteInfo].
*/
fun
TopFrecentSiteInfo
.
toTopSite
():
TopSite
{
return
TopSite
(
id
=
null
,
title
=
this
.
title
?.
takeIf
(
String
::
isNotBlank
)
?:
this
.
url
.
tryGetHostFromUrl
(),
url
=
this
.
url
,
createdAt
=
null
,
type
=
FRECENT
)
}
components/feature/top-sites/src/main/java/mozilla/components/feature/top/sites/ext/TopSite.kt
0 → 100644
View file @
c78e6a71
/* 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/. */
package
mozilla.components.feature.top.sites.ext
import
mozilla.components.feature.top.sites.TopSite
/**
* Returns true if the given url is in the list