Skip to content

Commit

Permalink
Use History on iOS (#2539)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/414235014887631/1206524433066955/f
Tech Design URL:
CC:

Description:
Adds history collection to iOS

Steps to test this PR:

See add history to ios BrowserServicesKit#693
In the manager's computed var for the coordinator where the store is loaded, change code to set an error message and ensure that the preemptive crash alert is shown.
  • Loading branch information
brindy authored Mar 11, 2024
1 parent c8f1501 commit 576c98f
Show file tree
Hide file tree
Showing 14 changed files with 640 additions and 34 deletions.
69 changes: 69 additions & 0 deletions Core/HistoryCapture.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// HistoryCapture.swift
// DuckDuckGo
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// 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.
//

import Foundation
import History

public class HistoryCapture {

enum VisitState {
case added
case expected
}

let historyManager: HistoryManaging
var coordinator: HistoryCoordinating {
historyManager.historyCoordinator
}

var url: URL?

public init(historyManager: HistoryManaging) {
self.historyManager = historyManager
}

public func webViewDidCommit(url: URL) {
self.url = url
coordinator.addVisit(of: url.urlOrDuckDuckGoCleanQuery)
}

public func titleDidChange(_ title: String?, forURL url: URL?) {
guard self.url == url else {
return
}

guard let url = url?.urlOrDuckDuckGoCleanQuery, let title, !title.isEmpty else {
return
}
coordinator.updateTitleIfNeeded(title: title, url: url)
coordinator.commitChanges(url: url)
}

}

extension URL {

var urlOrDuckDuckGoCleanQuery: URL {
guard isDuckDuckGoSearch,
let searchQuery,
let url = URL.makeSearchURL(query: searchQuery)?.removingInternalSearchParameters() else { return self }
return url
}

}
185 changes: 180 additions & 5 deletions Core/HistoryManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,196 @@
// limitations under the License.
//

import CoreData
import Foundation
import BrowserServicesKit
import History
import Common
import Persistence

class HistoryManager {
public protocol HistoryManaging {

let privacyConfig: PrivacyConfiguration
var historyCoordinator: HistoryCoordinating { get }
func loadStore()

}

public class HistoryManager: HistoryManaging {

let privacyConfigManager: PrivacyConfigurationManaging
let variantManager: VariantManager
let database: CoreDataDatabase
let onStoreLoadFailed: (Error) -> Void

private var currentHistoryCoordinator: HistoryCoordinating?

public var historyCoordinator: HistoryCoordinating {
guard isHistoryFeatureEnabled() else {
currentHistoryCoordinator = nil
return NullHistoryCoordinator()
}

if let currentHistoryCoordinator {
return currentHistoryCoordinator
}

var loadError: Error?
database.loadStore { _, error in
loadError = error
}

if let loadError {
onStoreLoadFailed(loadError)
return NullHistoryCoordinator()
}

init(privacyConfig: PrivacyConfiguration, variantManager: VariantManager) {
self.privacyConfig = privacyConfig
let context = database.makeContext(concurrencyType: .privateQueueConcurrencyType)
let historyCoordinator = HistoryCoordinator(historyStoring: HistoryStore(context: context, eventMapper: HistoryStoreEventMapper()))
currentHistoryCoordinator = historyCoordinator
return historyCoordinator
}

public init(privacyConfigManager: PrivacyConfigurationManaging, variantManager: VariantManager, database: CoreDataDatabase, onStoreLoadFailed: @escaping (Error) -> Void) {
self.privacyConfigManager = privacyConfigManager
self.variantManager = variantManager
self.database = database
self.onStoreLoadFailed = onStoreLoadFailed
}

func isHistoryFeatureEnabled() -> Bool {
return privacyConfig.isEnabled(featureKey: .history) && variantManager.isSupported(feature: .history)
return privacyConfigManager.privacyConfig.isEnabled(featureKey: .history) && variantManager.isSupported(feature: .history)
}

public func removeAllHistory() async {
await withCheckedContinuation { continuation in
historyCoordinator.burnAll {
continuation.resume()
}
}
}

public func loadStore() {
historyCoordinator.loadHistory {
// Do migrations here if needed
}
}

}

class NullHistoryCoordinator: HistoryCoordinating {

func loadHistory(onCleanFinished: @escaping () -> Void) {
}

var history: History.BrowsingHistory?

var allHistoryVisits: [History.Visit]?

@Published private(set) public var historyDictionary: [URL: HistoryEntry]?
var historyDictionaryPublisher: Published<[URL: History.HistoryEntry]?>.Publisher {
$historyDictionary
}

func addVisit(of url: URL) -> History.Visit? {
return nil
}

func addBlockedTracker(entityName: String, on url: URL) {
}

func trackerFound(on: URL) {
}

func updateTitleIfNeeded(title: String, url: URL) {
}

func markFailedToLoadUrl(_ url: URL) {
}

func commitChanges(url: URL) {
}

func title(for url: URL) -> String? {
return nil
}

func burnAll(completion: @escaping () -> Void) {
completion()
}

func burnDomains(_ baseDomains: Set<String>, tld: Common.TLD, completion: @escaping () -> Void) {
completion()
}

func burnVisits(_ visits: [History.Visit], completion: @escaping () -> Void) {
completion()
}

}

public class HistoryDatabase {

private init() { }

public static var defaultDBLocation: URL = {
guard let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
os_log("HistoryDatabase.make - OUT, failed to get application support directory")
fatalError("Failed to get location")
}
return url
}()

public static var defaultDBFileURL: URL = {
return defaultDBLocation.appendingPathComponent("History.sqlite", conformingTo: .database)
}()

public static func make(location: URL = defaultDBLocation, readOnly: Bool = false) -> CoreDataDatabase {
os_log("HistoryDatabase.make - IN - %s", location.absoluteString)
let bundle = History.bundle
guard let model = CoreDataDatabase.loadModel(from: bundle, named: "BrowsingHistory") else {
os_log("HistoryDatabase.make - OUT, failed to loadModel")
fatalError("Failed to load model")
}

let db = CoreDataDatabase(name: "History",
containerLocation: location,
model: model,
readOnly: readOnly)
os_log("HistoryDatabase.make - OUT")
return db
}
}

class HistoryStoreEventMapper: EventMapping<HistoryStore.HistoryStoreEvents> {
public init() {
super.init { event, error, _, _ in
switch event {
case .removeFailed:
Pixel.fire(pixel: .historyRemoveFailed, error: error)

case .reloadFailed:
Pixel.fire(pixel: .historyReloadFailed, error: error)

case .cleanEntriesFailed:
Pixel.fire(pixel: .historyCleanEntriesFailed, error: error)

case .cleanVisitsFailed:
Pixel.fire(pixel: .historyCleanVisitsFailed, error: error)

case .saveFailed:
Pixel.fire(pixel: .historySaveFailed, error: error)

case .insertVisitFailed:
Pixel.fire(pixel: .historyInsertVisitFailed, error: error)

case .removeVisitsFailed:
Pixel.fire(pixel: .historyRemoveVisitsFailed, error: error)
}

}
}

override init(mapping: @escaping EventMapping<HistoryStore.HistoryStoreEvents>.Mapping) {
fatalError("Use init()")
}
}
20 changes: 20 additions & 0 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,16 @@ extension Pixel {
case userBehaviorFireButtonAndRestart
case userBehaviorFireButtonAndTogglePrivacyControls

// MARK: History
case historyStoreLoadFailed
case historyRemoveFailed
case historyReloadFailed
case historyCleanEntriesFailed
case historyCleanVisitsFailed
case historySaveFailed
case historyInsertVisitFailed
case historyRemoveVisitsFailed

// MARK: Privacy pro
case privacyProSubscriptionActive
case privacyProOfferScreenImpression
Expand Down Expand Up @@ -1113,6 +1123,16 @@ extension Pixel.Event {
case .userBehaviorFireButtonAndRestart: return "m_fire-button-and-restart"
case .userBehaviorFireButtonAndTogglePrivacyControls: return "m_fire-button-and-toggle-privacy-controls"

// MARK: - History debug
case .historyStoreLoadFailed: return "m_debug_history-store-load-failed"
case .historyRemoveFailed: return "m_debug_history-remove-failed"
case .historyReloadFailed: return "m_debug_history-reload-failed"
case .historyCleanEntriesFailed: return "m_debug_history-clean-entries-failed"
case .historyCleanVisitsFailed: return "m_debug_history-clean-visits-failed"
case .historySaveFailed: return "m_debug_history-save-failed"
case .historyInsertVisitFailed: return "m_debug_history-insert-visit-failed"
case .historyRemoveVisitsFailed: return "m_debug_history-remove-visits-failed"

// MARK: Privacy pro
case .privacyProSubscriptionActive: return "m_privacy-pro_app_subscription_active"
case .privacyProOfferScreenImpression: return "m_privacy-pro_offer_screen_impression"
Expand Down
Loading

0 comments on commit 576c98f

Please sign in to comment.