PlacesHistoryStorage.kt 8.03 KB
Newer Older
1
2
3
4
5
6
7
/* 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.browser.storage.sync

import android.content.Context
8
import kotlinx.coroutines.withContext
9
import mozilla.appservices.places.PlacesApi
10
import mozilla.appservices.places.PlacesException
11
import mozilla.appservices.places.VisitObservation
12
import mozilla.components.concept.storage.HistoryAutocompleteResult
13
14
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.concept.storage.PageObservation
15
import mozilla.components.concept.storage.PageVisit
16
import mozilla.components.concept.storage.SearchResult
17
import mozilla.components.concept.storage.RedirectSource
18
import mozilla.components.concept.storage.VisitInfo
19
import mozilla.components.concept.storage.VisitType
20
import mozilla.components.concept.sync.SyncAuthInfo
21
import mozilla.components.concept.sync.SyncStatus
22
import mozilla.components.concept.sync.SyncableStore
23
import mozilla.components.support.base.log.logger.Logger
24
import mozilla.components.support.utils.segmentAwareDomainMatch
25

26
27
const val AUTOCOMPLETE_SOURCE_NAME = "placesHistory"

28
/**
29
 * Implementation of the [HistoryStorage] which is backed by a Rust Places lib via [PlacesApi].
30
 */
31
@SuppressWarnings("TooManyFunctions")
32
open class PlacesHistoryStorage(context: Context) : PlacesStorage(context), HistoryStorage, SyncableStore {
33

34
35
    override val logger = Logger("PlacesHistoryStorage")

36
    override suspend fun recordVisit(uri: String, visit: PageVisit) {
37
        withContext(scope.coroutineContext) {
38
39
40
            // Ignore exceptions related to uris. This means we may drop some of the data on the floor
            // if the underlying storage layer refuses it.
            ignoreUrlExceptions("recordVisit") {
41
42
43
44
45
46
47
48
49
50
51
                places.writer().noteObservation(VisitObservation(uri,
                    visitType = visit.visitType.into(),
                    isRedirectSource = when (visit.redirectSource) {
                        RedirectSource.PERMANENT, RedirectSource.TEMPORARY -> true
                        RedirectSource.NOT_A_SOURCE -> false
                    },
                    isPermanentRedirectSource = when (visit.redirectSource) {
                        RedirectSource.PERMANENT -> true
                        RedirectSource.TEMPORARY, RedirectSource.NOT_A_SOURCE -> false
                    }
                ))
52
            }
53
        }
54
55
56
    }

    override suspend fun recordObservation(uri: String, observation: PageObservation) {
57
58
        // NB: visitType 'UPDATE_PLACE' means "record meta information about this URL".
        withContext(scope.coroutineContext) {
59
60
61
62
63
64
65
66
67
68
69
            // Ignore exceptions related to uris. This means we may drop some of the data on the floor
            // if the underlying storage layer refuses it.
            ignoreUrlExceptions("recordObservation") {
                places.writer().noteObservation(
                        VisitObservation(
                                url = uri,
                                visitType = mozilla.appservices.places.VisitType.UPDATE_PLACE,
                                title = observation.title
                        )
                )
            }
70
        }
71
72
    }

73
    override suspend fun getVisited(uris: List<String>): List<Boolean> {
74
        return withContext(scope.coroutineContext) { places.reader().getVisited(uris) }
75
76
    }

77
    override suspend fun getVisited(): List<String> {
78
        return withContext(scope.coroutineContext) {
79
            places.reader().getVisitedUrlsInRange(
80
81
82
                    start = 0,
                    end = System.currentTimeMillis(),
                    includeRemote = true
83
            )
84
85
86
        }
    }

87
    override suspend fun getDetailedVisits(start: Long, end: Long, excludeTypes: List<VisitType>): List<VisitInfo> {
88
        return withContext(scope.coroutineContext) {
89
90
91
92
93
94
95
            places.reader().getVisitInfos(start, end, excludeTypes.map { it.into() }).map { it.into() }
        }
    }

    override suspend fun getVisitsPaginated(offset: Long, count: Long, excludeTypes: List<VisitType>): List<VisitInfo> {
        return withContext(scope.coroutineContext) {
            places.reader().getVisitPage(offset, count, excludeTypes.map { it.into() }).map { it.into() }
96
        }
97
98
99
100
    }

    override fun getSuggestions(query: String, limit: Int): List<SearchResult> {
        require(limit >= 0) { "Limit must be a positive integer" }
101
        return places.reader().queryAutocomplete(query, limit = limit).map {
102
103
104
105
            SearchResult(it.url, it.url, it.frecency.toInt(), it.title)
        }
    }

106
    override fun getAutocompleteSuggestion(query: String): HistoryAutocompleteResult? {
107
        val url = places.reader().matchUrl(query) ?: return null
108
109

        val resultText = segmentAwareDomainMatch(query, arrayListOf(url))
110
        return resultText?.let {
111
            HistoryAutocompleteResult(
112
113
114
115
116
                    input = query,
                    text = it.matchedSegment,
                    url = it.url,
                    source = AUTOCOMPLETE_SOURCE_NAME,
                    totalItems = 1
117
            )
118
        }
119
120
    }

121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
    /**
     * Sync behaviour: will not remove any history from remote devices, but it will prevent deleted
     * history from returning.
     */
    override suspend fun deleteEverything() {
        withContext(scope.coroutineContext) {
            places.writer().deleteEverything()
        }
    }

    /**
     * Sync behaviour: may remove history from remote devices, if the removed visits were the only
     * ones for a URL.
     */
    override suspend fun deleteVisitsSince(since: Long) {
        withContext(scope.coroutineContext) {
            places.writer().deleteVisitsSince(since)
        }
    }

    /**
     * Sync behaviour: may remove history from remote devices, if the removed visits were the only
     * ones for a URL.
     */
    override suspend fun deleteVisitsBetween(startTime: Long, endTime: Long) {
        withContext(scope.coroutineContext) {
            places.writer().deleteVisitsBetween(startTime, endTime)
        }
    }

    /**
     * Sync behaviour: will remove history from remote devices.
     */
    override suspend fun deleteVisitsFor(url: String) {
        withContext(scope.coroutineContext) {
            places.writer().deletePlace(url)
        }
    }

160
161
162
163
164
165
166
167
168
169
    /**
     * Sync behaviour: will remove history from remote devices if this was the only visit for [url].
     * Otherwise, remote devices are not affected.
     */
    override suspend fun deleteVisit(url: String, timestamp: Long) {
        withContext(scope.coroutineContext) {
            places.writer().deleteVisit(url, timestamp)
        }
    }

170
171
172
173
174
175
176
177
178
179
    /**
     * Should only be called in response to severe disk storage pressure. May delete all of the data,
     * or some subset of it.
     * Sync behaviour: will not remove history from remote clients.
     */
    override suspend fun prune() {
        withContext(scope.coroutineContext) {
            places.writer().pruneDestructively()
        }
    }
180
181
182
183
184
185
186

    /**
     * Runs syncHistory() method on the places Connection
     *
     * @param authInfo The authentication information to sync with.
     * @return Sync status of OK or Error
     */
187
    override suspend fun sync(authInfo: SyncAuthInfo): SyncStatus {
188
189
        return withContext(scope.coroutineContext) {
            syncAndHandleExceptions {
190
                places.syncHistory(authInfo)
191
192
193
            }
        }
    }
194

195
196
197
198
199
200
201
202
203
204
    /**
     * Import history and visits data from Fennec's browser.db file.
     *
     * @param dbPath Absolute path to Fennec's browser.db file.
     */
    @Throws(PlacesException::class)
    fun importFromFennec(dbPath: String) {
        places.importVisitsFromFennec(dbPath)
    }

205
206
207
208
209
210
211
212
    /**
     * This should be removed. See: https://github.com/mozilla/application-services/issues/1877
     *
     * @return raw internal handle that could be used for referencing underlying [PlacesApi]. Use it with SyncManager.
     */
    override fun getHandle(): Long {
        return places.getHandle()
    }
213
}