1use crate::api::matcher::{self, search_frecent, SearchParams};
8pub use crate::api::places_api::places_api_new;
9pub use crate::error::{warn, Result};
10pub use crate::error::{ApiResult, PlacesApiError};
11#[cfg(all(feature = "glean-sym", target_os = "android"))]
12use crate::glean_metrics::places_manager;
13pub use crate::import::common::HistoryMigrationResult;
14use crate::import::import_ios_history;
15use crate::storage;
16use crate::storage::bookmarks;
17pub use crate::storage::bookmarks::BookmarkPosition;
18pub use crate::storage::history_metadata::{
19 DocumentType, HistoryHighlight, HistoryHighlightWeights, HistoryMetadata,
20 HistoryMetadataObservation, HistoryMetadataPageMissingBehavior,
21 NoteHistoryMetadataObservationOptions,
22};
23pub use crate::storage::RunMaintenanceMetrics;
24use crate::storage::{history, history_metadata};
25use crate::types::VisitTransitionSet;
26use crate::ConnectionType;
27use crate::VisitObservation;
28use crate::VisitType;
29use crate::{PlacesApi, PlacesDb};
30use error_support::handle_error;
31use interrupt_support::register_interrupt;
32pub use interrupt_support::SqlInterruptHandle;
33use parking_lot::Mutex;
34use std::sync::{Arc, Weak};
35pub use sync_guid::Guid;
36pub use types::Timestamp as PlacesTimestamp;
37pub use url::Url;
38
39const SKIP_ONE_PAGE_FRECENCY_THRESHOLD: i64 = 101 + 1;
41
42pub type InsertableBookmarkItem = crate::storage::bookmarks::InsertableItem;
45pub type InsertableBookmarkFolder = crate::storage::bookmarks::InsertableFolder;
46pub type InsertableBookmarkSeparator = crate::storage::bookmarks::InsertableSeparator;
47pub use crate::storage::bookmarks::InsertableBookmark;
48
49pub use crate::storage::bookmarks::BookmarkUpdateInfo;
50
51pub type BookmarkItem = crate::storage::bookmarks::fetch::Item;
53pub type BookmarkFolder = crate::storage::bookmarks::fetch::Folder;
54pub type BookmarkSeparator = crate::storage::bookmarks::fetch::Separator;
55pub use crate::storage::bookmarks::fetch::BookmarkData;
56
57uniffi::custom_type!(Url, String, {
58 remote,
59 try_lift: |val| {
60 match Url::parse(val.as_str()) {
61 Ok(url) => Ok(url),
62 Err(e) => Err(PlacesApiError::UrlParseFailed {
63 reason: e.to_string(),
64 }
65 .into()),
66 }
67 },
68 lower: |obj| obj.into(),
69});
70
71uniffi::custom_type!(PlacesTimestamp, i64, {
72 remote,
73 try_lift: |val| Ok(PlacesTimestamp(val as u64)),
74 lower: |obj| obj.as_millis() as i64,
75});
76
77uniffi::custom_type!(VisitTransitionSet, i32, {
78 try_lift: |val| {
79 Ok(VisitTransitionSet::from_u16(val as u16).expect("Bug: Invalid VisitTransitionSet"))
80 },
81 lower: |obj| VisitTransitionSet::into_u16(obj) as i32,
82});
83
84uniffi::custom_type!(Guid, String, {
85 remote,
86 try_lift: |val| Ok(Guid::new(val.as_str())),
87 lower: |obj| obj.into(),
88});
89
90lazy_static::lazy_static! {
95 static ref READ_WRITE_CONNECTIONS: Mutex<Vec<Weak<PlacesConnection>>> = Mutex::new(Vec::new());
96 static ref SYNC_CONNECTIONS: Mutex<Vec<Weak<PlacesConnection>>> = Mutex::new(Vec::new());
97}
98
99impl PlacesApi {
100 #[handle_error(crate::Error)]
101 pub fn new_connection(&self, conn_type: ConnectionType) -> ApiResult<Arc<PlacesConnection>> {
102 let db = self.open_connection(conn_type)?;
103 let connection = Arc::new(PlacesConnection::new(db));
104 register_interrupt(Arc::<PlacesConnection>::downgrade(&connection));
105 Ok(connection)
106 }
107}
108
109pub struct PlacesConnection {
110 db: Mutex<PlacesDb>,
111 interrupt_handle: Arc<SqlInterruptHandle>,
112}
113
114impl PlacesConnection {
115 pub fn new(db: PlacesDb) -> Self {
116 #[cfg(all(feature = "glean-sym", target_os = "android"))]
117 places_manager::connection_initialized.add(1);
118
119 Self {
120 interrupt_handle: db.new_interrupt_handle(),
121 db: Mutex::new(db),
122 }
123 }
124
125 fn with_conn<F, T>(&self, f: F) -> Result<T>
127 where
128 F: FnOnce(&PlacesDb) -> crate::error::Result<T>,
129 {
130 let conn = self.db.lock();
131 f(&conn)
132 }
133
134 pub fn new_interrupt_handle(&self) -> Arc<SqlInterruptHandle> {
136 Arc::clone(&self.interrupt_handle)
137 }
138
139 #[handle_error(crate::Error)]
140 pub fn get_latest_history_metadata_for_url(
141 &self,
142 url: Url,
143 ) -> ApiResult<Option<HistoryMetadata>> {
144 self.with_conn(|conn| history_metadata::get_latest_for_url(conn, &url))
145 }
146
147 #[handle_error(crate::Error)]
148 pub fn get_history_metadata_between(
149 &self,
150 start: PlacesTimestamp,
151 end: PlacesTimestamp,
152 ) -> ApiResult<Vec<HistoryMetadata>> {
153 self.with_conn(|conn| {
154 history_metadata::get_between(conn, start.as_millis_i64(), end.as_millis_i64())
155 })
156 }
157
158 #[handle_error(crate::Error)]
159 pub fn get_history_metadata_since(
160 &self,
161 start: PlacesTimestamp,
162 ) -> ApiResult<Vec<HistoryMetadata>> {
163 self.with_conn(|conn| history_metadata::get_since(conn, start.as_millis_i64()))
164 }
165
166 #[handle_error(crate::Error)]
167 pub fn get_most_recent_history_metadata(&self, limit: i32) -> ApiResult<Vec<HistoryMetadata>> {
168 self.with_conn(|conn| history_metadata::get_most_recent(conn, limit))
169 }
170
171 #[handle_error(crate::Error)]
172 pub fn get_most_recent_search_entries_in_history_metadata(
173 &self,
174 limit: i32,
175 ) -> ApiResult<Vec<HistoryMetadata>> {
176 self.with_conn(|conn| history_metadata::get_most_recent_search_entries(conn, limit))
177 }
178
179 #[handle_error(crate::Error)]
180 pub fn query_history_metadata(
181 &self,
182 query: String,
183 limit: i32,
184 ) -> ApiResult<Vec<HistoryMetadata>> {
185 self.with_conn(|conn| history_metadata::query(conn, query.as_str(), limit))
186 }
187
188 #[handle_error(crate::Error)]
189 pub fn get_history_highlights(
190 &self,
191 weights: HistoryHighlightWeights,
192 limit: i32,
193 ) -> ApiResult<Vec<HistoryHighlight>> {
194 self.with_conn(|conn| history_metadata::get_highlights(conn, weights, limit))
195 }
196
197 #[handle_error(crate::Error)]
198 pub fn note_history_metadata_observation(
199 &self,
200 data: HistoryMetadataObservation,
201 options: NoteHistoryMetadataObservationOptions,
202 ) -> ApiResult<()> {
203 self.with_conn(|conn| history_metadata::apply_metadata_observation(conn, data, options))
205 }
206
207 #[handle_error(crate::Error)]
208 pub fn metadata_delete_older_than(&self, older_than: PlacesTimestamp) -> ApiResult<()> {
209 self.with_conn(|conn| history_metadata::delete_older_than(conn, older_than.as_millis_i64()))
210 }
211
212 #[handle_error(crate::Error)]
213 pub fn metadata_delete(
214 &self,
215 url: Url,
216 referrer_url: Option<Url>,
217 search_term: Option<String>,
218 ) -> ApiResult<()> {
219 self.with_conn(|conn| {
220 history_metadata::delete_metadata(
221 conn,
222 &url,
223 referrer_url.as_ref(),
224 search_term.as_deref(),
225 )
226 })
227 }
228
229 #[handle_error(crate::Error)]
230 pub fn metadata_delete_search_terms(&self) -> ApiResult<()> {
231 self.with_conn(history_metadata::delete_all_metadata_for_search)
232 }
233
234 #[handle_error(crate::Error)]
236 pub fn apply_observation(&self, visit: VisitObservation) -> ApiResult<()> {
237 self.with_conn(|conn| history::apply_observation(conn, visit))?;
238 Ok(())
239 }
240
241 #[handle_error(crate::Error)]
242 pub fn get_visited_urls_in_range(
243 &self,
244 start: PlacesTimestamp,
245 end: PlacesTimestamp,
246 include_remote: bool,
247 ) -> ApiResult<Vec<Url>> {
248 self.with_conn(|conn| {
249 let urls = history::get_visited_urls(conn, start, end, include_remote)?
250 .iter()
251 .filter_map(|s| Url::parse(s).ok())
253 .collect::<Vec<_>>();
254 Ok(urls)
255 })
256 }
257
258 #[handle_error(crate::Error)]
259 pub fn get_visit_infos(
260 &self,
261 start_date: PlacesTimestamp,
262 end_date: PlacesTimestamp,
263 exclude_types: VisitTransitionSet,
264 ) -> ApiResult<Vec<HistoryVisitInfo>> {
265 self.with_conn(|conn| history::get_visit_infos(conn, start_date, end_date, exclude_types))
266 }
267
268 #[handle_error(crate::Error)]
269 pub fn get_visit_count(&self, exclude_types: VisitTransitionSet) -> ApiResult<i64> {
270 self.with_conn(|conn| history::get_visit_count(conn, exclude_types))
271 }
272
273 #[handle_error(crate::Error)]
274 pub fn get_visit_count_for_host(
275 &self,
276 host: String,
277 before: PlacesTimestamp,
278 exclude_types: VisitTransitionSet,
279 ) -> ApiResult<i64> {
280 self.with_conn(|conn| {
281 history::get_visit_count_for_host(conn, host.as_str(), before, exclude_types)
282 })
283 }
284
285 #[handle_error(crate::Error)]
286 pub fn get_visit_page(
287 &self,
288 offset: i64,
289 count: i64,
290 exclude_types: VisitTransitionSet,
291 ) -> ApiResult<Vec<HistoryVisitInfo>> {
292 self.with_conn(|conn| history::get_visit_page(conn, offset, count, exclude_types))
293 }
294
295 #[handle_error(crate::Error)]
296 pub fn get_visit_page_with_bound(
297 &self,
298 bound: i64,
299 offset: i64,
300 count: i64,
301 exclude_types: VisitTransitionSet,
302 ) -> ApiResult<HistoryVisitInfosWithBound> {
303 self.with_conn(|conn| {
304 history::get_visit_page_with_bound(conn, bound, offset, count, exclude_types)
305 })
306 }
307
308 #[handle_error(crate::Error)]
312 pub fn get_visited(&self, urls: Vec<String>) -> ApiResult<Vec<bool>> {
313 let iter = urls.into_iter();
314 let mut result = vec![false; iter.len()];
315 let url_idxs = iter
316 .enumerate()
317 .filter_map(|(idx, s)| Url::parse(&s).ok().map(|url| (idx, url)))
318 .collect::<Vec<_>>();
319 self.with_conn(|conn| history::get_visited_into(conn, &url_idxs, &mut result))?;
320 Ok(result)
321 }
322
323 #[handle_error(crate::Error)]
324 pub fn delete_visits_for(&self, url: String) -> ApiResult<()> {
325 self.with_conn(|conn| {
326 let guid = match Url::parse(&url) {
327 Ok(url) => history::url_to_guid(conn, &url)?,
328 Err(e) => {
329 warn!("Invalid URL passed to places_delete_visits_for, {}", e);
330 history::href_to_guid(conn, url.clone().as_str())?
331 }
332 };
333 if let Some(guid) = guid {
334 history::delete_visits_for(conn, &guid)?;
335 }
336 Ok(())
337 })
338 }
339
340 #[handle_error(crate::Error)]
341 pub fn delete_visits_between(
342 &self,
343 start: PlacesTimestamp,
344 end: PlacesTimestamp,
345 ) -> ApiResult<()> {
346 self.with_conn(|conn| history::delete_visits_between(conn, start, end))
347 }
348
349 #[handle_error(crate::Error)]
350 pub fn delete_visit(&self, url: String, timestamp: PlacesTimestamp) -> ApiResult<()> {
351 self.with_conn(|conn| {
352 match Url::parse(&url) {
353 Ok(url) => {
354 history::delete_place_visit_at_time(conn, &url, timestamp)?;
355 }
356 Err(e) => {
357 warn!("Invalid URL passed to places_delete_visit, {}", e);
358 history::delete_place_visit_at_time_by_href(conn, url.as_str(), timestamp)?;
359 }
360 };
361 Ok(())
362 })
363 }
364
365 #[handle_error(crate::Error)]
366 pub fn get_top_frecent_site_infos(
367 &self,
368 num_items: i32,
369 threshold_option: FrecencyThresholdOption,
370 ) -> ApiResult<Vec<TopFrecentSiteInfo>> {
371 self.with_conn(|conn| {
372 crate::storage::history::get_top_frecent_site_infos(
373 conn,
374 num_items,
375 threshold_option.value(),
376 )
377 })
378 }
379 #[handle_error(crate::Error)]
382 pub fn delete_everything_history(&self) -> ApiResult<()> {
383 history::delete_everything(&self.db.lock())
384 }
385
386 #[handle_error(crate::Error)]
387 pub fn run_maintenance_prune(
388 &self,
389 db_size_limit: u32,
390 prune_limit: u32,
391 ) -> ApiResult<RunMaintenanceMetrics> {
392 #[cfg(all(feature = "glean-sym", target_os = "android"))]
393 let timer_id = places_manager::run_maintenance_prune_time_temp.start();
394 let res =
395 self.with_conn(|conn| storage::run_maintenance_prune(conn, db_size_limit, prune_limit));
396
397 #[cfg(all(feature = "glean-sym", target_os = "android"))]
398 places_manager::run_maintenance_prune_time_temp.stop_and_accumulate(timer_id);
399
400 res
401 }
402
403 #[handle_error(crate::Error)]
404 pub fn run_maintenance_vacuum(&self) -> ApiResult<()> {
405 #[cfg(all(feature = "glean-sym", target_os = "android"))]
406 let timer_id = places_manager::run_maintenance_vacuum_time_temp.start();
407 let res = self.with_conn(storage::run_maintenance_vacuum);
408
409 #[cfg(all(feature = "glean-sym", target_os = "android"))]
410 places_manager::run_maintenance_vacuum_time_temp.stop_and_accumulate(timer_id);
411
412 res
413 }
414
415 #[handle_error(crate::Error)]
416 pub fn run_maintenance_optimize(&self) -> ApiResult<()> {
417 #[cfg(all(feature = "glean-sym", target_os = "android"))]
418 let timer_id = places_manager::run_maintenance_optimize_time_temp.start();
419 let res = self.with_conn(storage::run_maintenance_optimize);
420
421 #[cfg(all(feature = "glean-sym", target_os = "android"))]
422 places_manager::run_maintenance_optimize_time_temp.stop_and_accumulate(timer_id);
423
424 res
425 }
426
427 #[handle_error(crate::Error)]
428 pub fn run_maintenance_checkpoint(&self) -> ApiResult<()> {
429 #[cfg(all(feature = "glean-sym", target_os = "android"))]
430 let timer_id = places_manager::run_maintenance_chk_pnt_time_temp.start();
431 let res = self.with_conn(storage::run_maintenance_checkpoint);
432
433 #[cfg(all(feature = "glean-sym", target_os = "android"))]
434 places_manager::run_maintenance_chk_pnt_time_temp.stop_and_accumulate(timer_id);
435
436 res
437 }
438
439 #[handle_error(crate::Error)]
440 pub fn query_autocomplete(&self, search: String, limit: i32) -> ApiResult<Vec<SearchResult>> {
441 self.with_conn(|conn| {
442 search_frecent(
443 conn,
444 SearchParams {
445 search_string: search,
446 limit: limit as u32,
447 },
448 )
449 .map(|search_results| search_results.into_iter().map(Into::into).collect())
450 })
451 }
452
453 #[handle_error(crate::Error)]
454 pub fn accept_result(&self, search_string: String, url: String) -> ApiResult<()> {
455 self.with_conn(|conn| {
456 match Url::parse(&url) {
457 Ok(url) => {
458 matcher::accept_result(conn, &search_string, &url)?;
459 }
460 Err(_) => {
461 warn!("Ignoring invalid URL in places_accept_result");
462 return Ok(());
463 }
464 };
465 Ok(())
466 })
467 }
468
469 #[handle_error(crate::Error)]
470 pub fn match_url(&self, query: String) -> ApiResult<Option<Url>> {
471 self.with_conn(|conn| matcher::match_url(conn, query))
472 }
473
474 #[handle_error(crate::Error)]
475 pub fn bookmarks_get_tree(&self, item_guid: &Guid) -> ApiResult<Option<BookmarkItem>> {
476 self.with_conn(|conn| bookmarks::fetch::fetch_tree(conn, item_guid))
477 }
478
479 #[handle_error(crate::Error)]
480 pub fn bookmarks_get_by_guid(
481 &self,
482 guid: &Guid,
483 get_direct_children: bool,
484 ) -> ApiResult<Option<BookmarkItem>> {
485 self.with_conn(|conn| {
486 let bookmark = bookmarks::fetch::fetch_bookmark(conn, guid, get_direct_children)?;
487 Ok(bookmark)
488 })
489 }
490
491 #[handle_error(crate::Error)]
492 pub fn bookmarks_get_all_with_url(&self, url: String) -> ApiResult<Vec<BookmarkItem>> {
493 self.with_conn(|conn| {
494 match Url::parse(&url) {
496 Ok(url) => Ok(bookmarks::fetch::fetch_bookmarks_by_url(conn, &url)?
497 .into_iter()
498 .map(|b| BookmarkItem::Bookmark { b })
499 .collect::<Vec<BookmarkItem>>()),
500 Err(e) => {
501 warn!("Invalid URL passed to bookmarks_get_all_with_url, {}", e);
503 Ok(Vec::<BookmarkItem>::new())
504 }
505 }
506 })
507 }
508
509 #[handle_error(crate::Error)]
510 pub fn bookmarks_search(&self, query: String, limit: i32) -> ApiResult<Vec<BookmarkItem>> {
511 self.with_conn(|conn| {
512 Ok(
514 bookmarks::fetch::search_bookmarks(conn, query.as_str(), limit as u32)?
515 .into_iter()
516 .map(|b| BookmarkItem::Bookmark { b })
517 .collect(),
518 )
519 })
520 }
521
522 #[handle_error(crate::Error)]
523 pub fn bookmarks_get_recent(&self, limit: i32) -> ApiResult<Vec<BookmarkItem>> {
524 self.with_conn(|conn| {
525 Ok(bookmarks::fetch::recent_bookmarks(conn, limit as u32)?
527 .into_iter()
528 .map(|b| BookmarkItem::Bookmark { b })
529 .collect())
530 })
531 }
532
533 #[handle_error(crate::Error)]
534 pub fn bookmarks_delete(&self, id: Guid) -> ApiResult<bool> {
535 self.with_conn(|conn| bookmarks::delete_bookmark(conn, &id))
536 }
537
538 #[handle_error(crate::Error)]
539 pub fn bookmarks_delete_everything(&self) -> ApiResult<()> {
540 self.with_conn(bookmarks::delete_everything)
541 }
542
543 #[handle_error(crate::Error)]
544 pub fn bookmarks_get_url_for_keyword(&self, keyword: String) -> ApiResult<Option<Url>> {
545 self.with_conn(|conn| bookmarks::bookmarks_get_url_for_keyword(conn, keyword.as_str()))
546 }
547
548 #[handle_error(crate::Error)]
549 pub fn bookmarks_insert(&self, data: InsertableBookmarkItem) -> ApiResult<Guid> {
550 self.with_conn(|conn| bookmarks::insert_bookmark(conn, data))
551 }
552
553 #[handle_error(crate::Error)]
554 pub fn bookmarks_update(&self, item: BookmarkUpdateInfo) -> ApiResult<()> {
555 self.with_conn(|conn| bookmarks::update_bookmark_from_info(conn, item))
556 }
557
558 #[handle_error(crate::Error)]
559 pub fn bookmarks_count_bookmarks_in_trees(&self, guids: &[Guid]) -> ApiResult<u32> {
560 self.with_conn(|conn| bookmarks::count_bookmarks_in_trees(conn, guids))
561 }
562
563 #[handle_error(crate::Error)]
564 pub fn places_history_import_from_ios(
565 &self,
566 db_path: String,
567 last_sync_timestamp: i64,
568 ) -> ApiResult<HistoryMigrationResult> {
569 self.with_conn(|conn| import_ios_history(conn, &db_path, last_sync_timestamp))
570 }
571}
572
573impl AsRef<SqlInterruptHandle> for PlacesConnection {
574 fn as_ref(&self) -> &SqlInterruptHandle {
575 &self.interrupt_handle
576 }
577}
578
579#[derive(Clone, PartialEq, Eq)]
580pub struct HistoryVisitInfo {
581 pub url: Url,
582 pub title: Option<String>,
583 pub timestamp: PlacesTimestamp,
584 pub visit_type: VisitType,
585 pub is_hidden: bool,
586 pub preview_image_url: Option<Url>,
587 pub is_remote: bool,
588}
589#[derive(Clone, PartialEq, Eq)]
590pub struct HistoryVisitInfosWithBound {
591 pub infos: Vec<HistoryVisitInfo>,
592 pub bound: i64,
593 pub offset: i64,
594}
595
596pub struct TopFrecentSiteInfo {
597 pub url: Url,
598 pub title: Option<String>,
599}
600
601pub enum FrecencyThresholdOption {
602 None,
603 SkipOneTimePages,
604}
605
606impl FrecencyThresholdOption {
607 fn value(&self) -> i64 {
608 match self {
609 FrecencyThresholdOption::None => 0,
610 FrecencyThresholdOption::SkipOneTimePages => SKIP_ONE_PAGE_FRECENCY_THRESHOLD,
611 }
612 }
613}
614
615pub struct SearchResult {
616 pub url: Url,
617 pub title: String,
618 pub frecency: i64,
619}
620
621pub struct Dummy {
623 pub md: Option<Vec<HistoryMetadata>>,
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629 use crate::test::new_mem_connection;
630
631 #[test]
632 fn test_accept_result_with_invalid_url() {
633 let conn = PlacesConnection::new(new_mem_connection());
634 let invalid_url = "http://1234.56.78.90".to_string();
635 assert!(PlacesConnection::accept_result(&conn, "ample".to_string(), invalid_url).is_ok());
636 }
637
638 #[test]
639 fn test_bookmarks_get_all_with_url_with_invalid_url() {
640 let conn = PlacesConnection::new(new_mem_connection());
641 let invalid_url = "http://1234.56.78.90".to_string();
642 assert!(PlacesConnection::bookmarks_get_all_with_url(&conn, invalid_url).is_ok());
643 }
644}