Skip to content
This repository has been archived by the owner on Feb 24, 2025. It is now read-only.

Commit

Permalink
Adding to the Dock automatically (#2722)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/72649045549333/1206797051025460/f
Tech Design URL:
CC:

**Description**:
Adding to the Dock automatically during the onboarding, through the
new tab page card and Settings.
  • Loading branch information
tomasstrba authored May 21, 2024
1 parent 245d75a commit 5f20c6c
Show file tree
Hide file tree
Showing 25 changed files with 1,378 additions and 38 deletions.
24 changes: 24 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

146 changes: 146 additions & 0 deletions DuckDuckGo/Application/DockCustomizer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//
// DockCustomizer.swift
//
// 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 Common

protocol DockCustomization {
var isAddedToDock: Bool { get }

@discardableResult
func addToDock() -> Bool
}

final class DockCustomizer: DockCustomization {

private let positionProvider: DockPositionProviding

init(positionProvider: DockPositionProviding = DockPositionProvider()) {
self.positionProvider = positionProvider
}

private var dockPlistURL: URL = URL(fileURLWithPath: NSString(string: "~/Library/Preferences/com.apple.dock.plist").expandingTildeInPath)

private var dockPlistDict: [String: AnyObject]? {
return NSDictionary(contentsOf: dockPlistURL) as? [String: AnyObject]
}

// This checks whether the bundle identifier of the current bundle
// is present in the 'persistent-apps' array of the Dock's plist.
var isAddedToDock: Bool {
guard let bundleIdentifier = Bundle.main.bundleIdentifier,
let dockPlistDict = dockPlistDict,
let persistentApps = dockPlistDict["persistent-apps"] as? [[String: AnyObject]] else {
return false
}

return persistentApps.contains(where: { ($0["tile-data"] as? [String: AnyObject])?["bundle-identifier"] as? String == bundleIdentifier })
}

// Adds a dictionary representing the application, either by using an existing
// one from 'recent-apps' or creating a new one if the application isn't recently used.
// It then inserts this dictionary into the 'persistent-apps' list at a position
// determined by `positionProvider`. Following the plist update, it schedules the Dock
// to restart after a brief delay to apply the changes.
@discardableResult
func addToDock() -> Bool {
let appPath = Bundle.main.bundleURL.path
guard !isAddedToDock,
let bundleIdentifier = Bundle.main.bundleIdentifier,
var dockPlistDict = dockPlistDict else {
return false
}

var persistentApps = dockPlistDict["persistent-apps"] as? [[String: AnyObject]] ?? []
let recentApps = dockPlistDict["recent-apps"] as? [[String: AnyObject]] ?? []

let appDict: [String: AnyObject]
// Find the app in recent apps
if let recentAppIndex = recentApps.firstIndex(where: { appDict in
if let tileData = appDict["tile-data"] as? [String: AnyObject],
let appBundleIdentifier = tileData["bundle-identifier"] as? String {
return appBundleIdentifier == bundleIdentifier
}
return false
}) {
// Use existing dictonary from recentApps
appDict = recentApps[recentAppIndex]
} else {
// Create the dictionary for the current application if not found in recent apps
appDict = Self.appDict(appPath: appPath, bundleIdentifier: bundleIdentifier)
}

// Insert to persistent apps
let index = positionProvider.newDockIndex(from: makeAppURLs(from: persistentApps))
persistentApps.insert(appDict, at: index)

// Update the plist
dockPlistDict["persistent-apps"] = persistentApps as AnyObject?
dockPlistDict["recent-apps"] = recentApps as AnyObject?

// Update mod-count
dockPlistDict["mod-count"] = ((dockPlistDict["mod-count"] as? Int) ?? 0) + 1 as AnyObject

do {
try (dockPlistDict as NSDictionary).write(to: dockPlistURL)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.restartDock()
}
return true
} catch {
os_log(.error, "Error writing to Dock plist: %{public}@", error.localizedDescription)
return false
}
}

private func restartDock() {
let task = Process()
task.launchPath = "/usr/bin/killall"
task.arguments = ["Dock"]
task.launch()
}

private func makeAppURLs(from persistentApps: [[String: AnyObject]]) -> [URL] {
return persistentApps.compactMap { appDict in
if let tileData = appDict["tile-data"] as? [String: AnyObject],
let appBundleIdentifier = tileData["file-data"] as? [String: AnyObject],
let urlString = appBundleIdentifier["_CFURLString"] as? String,
let url = URL(string: urlString) {
return url
} else {
return nil
}
}
}

static func appDict(appPath: String, bundleIdentifier: String) -> [String: AnyObject] {
return ["tile-type": "file-tile" as AnyObject,
"tile-data": [
"dock-extra": 0 as AnyObject,
"file-type": 1 as AnyObject,
"file-data": [
"_CFURLString": "file://" + appPath + "/",
"_CFURLStringType": 15
],
"file-label": "DuckDuckGo" as AnyObject,
"bundle-identifier": bundleIdentifier as AnyObject,
"is-beta": 0 as AnyObject
] as AnyObject
]
}
}
79 changes: 79 additions & 0 deletions DuckDuckGo/Application/DockPositionProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// DockPositionProvider.swift
//
// 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

enum DockApp: String, CaseIterable {
case chrome = "/Applications/Google Chrome.app/"
case firefox = "/Applications/Firefox.app/"
case edge = "/Applications/Microsoft Edge.app/"
case brave = "/Applications/Brave Browser.app/"
case opera = "/Applications/Opera.app/"
case arc = "/Applications/Arc.app/"
case safari = "/Applications/Safari.app/"
case safariLong = "/System/Volumes/Preboot/Cryptexes/App/System/Applications/Safari.app/"

var url: URL {
return URL(string: "file://" + self.rawValue)!
}
}

protocol DockPositionProviding {
func newDockIndex(from currentAppURLs: [URL]) -> Int
}

/// Class to determine the best positioning in the Dock
final class DockPositionProvider: DockPositionProviding {

private let preferredOrder: [DockApp] = [
.chrome,
.firefox,
.edge,
.brave,
.opera,
.arc,
.safari,
.safariLong
]

private var defaultBrowserProvider: DefaultBrowserProvider

init(defaultBrowserProvider: DefaultBrowserProvider = SystemDefaultBrowserProvider()) {
self.defaultBrowserProvider = defaultBrowserProvider
}

/// Determines the new dock index for a new app based on the default browser or preferred order
func newDockIndex(from currentAppURLs: [URL]) -> Int {
// Place next to the default browser
if !defaultBrowserProvider.isDefault,
let defaultBrowserURL = defaultBrowserProvider.defaultBrowserURL,
let position = currentAppURLs.firstIndex(of: defaultBrowserURL) {
return position + 1
}

// Place based on the preferred order
for app in preferredOrder {
if let position = currentAppURLs.firstIndex(of: app.url) {
return position + 1
}
}

// Otherwise, place at the end
return currentAppURLs.count
}
}
12 changes: 12 additions & 0 deletions DuckDuckGo/Assets.xcassets/Images/Dock-128.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Dock-128.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
11 changes: 11 additions & 0 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,10 @@ struct UserText {
static let isDefaultBrowser = NSLocalizedString("preferences.default-browser.active", value: "DuckDuckGo is your default browser", comment: "Indicate that the browser is the default")
static let isNotDefaultBrowser = NSLocalizedString("preferences.default-browser.inactive", value: "DuckDuckGo is not your default browser.", comment: "Indicate that the browser is not the default")
static let makeDefaultBrowser = NSLocalizedString("preferences.default-browser.button.make-default", value: "Make DuckDuckGo Default…", comment: "represents a prompt message asking the user to make DuckDuckGo their default browser.")
static let shortcuts = NSLocalizedString("preferences.shortcuts", value: "Shortcuts", comment: "Name of the preferences section related to shortcuts")
static let isAddedToDock = NSLocalizedString("preferences.is-added-to-dock", value: "DuckDuckGo is added to the Dock.", comment: "Indicates that the browser is added to the macOS system Dock")
static let isNotAddedToDock = NSLocalizedString("preferences.not-added-to-dock", value: "DuckDuckGo is not added to the Dock.", comment: "Indicate that the browser is not added to macOS system Dock")
static let addToDock = NSLocalizedString("preferences.add-to-dock", value: "Add to Dock…", comment: "Action button to add the app to the Dock")
static let onStartup = NSLocalizedString("preferences.on-startup", value: "On Startup", comment: "Name of the preferences section related to app startup")
static let reopenAllWindowsFromLastSession = NSLocalizedString("preferences.reopen-windows", value: "Reopen all windows from last session", comment: "Option to control session restoration")
static let showHomePage = NSLocalizedString("preferences.show-home", value: "Open a new window", comment: "Option to control session startup")
Expand Down Expand Up @@ -794,11 +798,14 @@ struct UserText {
static let onboardingWelcomeText = NSLocalizedString("onboarding.welcome.text", value: "Tired of being tracked online? You've come to the right place 👍\n\nI'll help you stay private️ as you search and browse the web. Trackers be gone!", comment: "Detailed welcome to the app text")
static let onboardingImportDataText = NSLocalizedString("onboarding.importdata.text", value: "First, let me help you import your bookmarks 📖 and passwords 🔑 from those less private browsers.", comment: "Call to action to import data from other browsers")
static let onboardingSetDefaultText = NSLocalizedString("onboarding.setdefault.text", value: "Next, try setting DuckDuckGo as your default️ browser, so you can open links with peace of mind, every time.", comment: "Call to action to set the browser as default")
static let onboardingAddToDockText = NSLocalizedString("onboarding.addtodock.text", value: "One last thing. Want to keep DuckDuckGo in your Dock so the browser's always within reach?", comment: "Call to action to add the DuckDuckGo app icon to the macOS system dock")
static let onboardingStartBrowsingText = NSLocalizedString("onboarding.startbrowsing.text", value: "You’re all set!\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible\u{00A0}🔒", comment: "Call to action to start using the app as a browser")
static let onboardingStartBrowsingAddedToDockText = NSLocalizedString("onboarding.startbrowsing.added-to-dock.text", value: "You’re all set! You can find me hanging out in the Dock anytime.\n\nWant to see how I protect you? Try visiting one of your favorite sites 👆\n\nKeep watching the address bar as you go. I’ll be blocking trackers and upgrading the security of your connection when possible\u{00A0}🔒", comment: "Call to action to start using the app as a browser")

static let onboardingStartButton = NSLocalizedString("onboarding.welcome.button", value: "Get Started", comment: "Start the onboarding flow")
static let onboardingImportDataButton = NSLocalizedString("onboarding.importdata.button", value: "Import", comment: "Launch the import data UI")
static let onboardingSetDefaultButton = NSLocalizedString("onboarding.setdefault.button", value: "Let's Do It!", comment: "Launch the set default UI")
static let onboardingAddToDockButton = NSLocalizedString("onboarding.addtodock.button", value: "Keep in Dock", comment: "Button label to add application to the macOS system dock")
static let onboardingNotNowButton = NSLocalizedString("onboarding.notnow.button", value: "Maybe Later", comment: "Skip a step of the onboarding flow")

static func importingBookmarks(_ numberOfBookmarks: Int?) -> String {
Expand Down Expand Up @@ -1070,17 +1077,21 @@ struct UserText {
// Set Up
static let newTabSetUpSectionTitle = NSLocalizedString("newTab.setup.section.title", value: "Next Steps", comment: "Title of the setup section in the home page")
static let newTabSetUpDefaultBrowserCardTitle = NSLocalizedString("newTab.setup.default.browser.title", value: "Default to Privacy", comment: "Title of the Default Browser card of the Set Up section in the home page")
static let newTabSetUpDockCardTitle = NSLocalizedString("newTab.setup.dock.title", value: "Keep in Your Dock", comment: "Title of the new tab page card for adding application to the Dock")
static let newTabSetUpImportCardTitle = NSLocalizedString("newTab.setup.import.title", value: "Bring Your Stuff", comment: "Title of the Import card of the Set Up section in the home page")
static let newTabSetUpDuckPlayerCardTitle = NSLocalizedString("newTab.setup.duck.player.title", value: "Clean Up YouTube", comment: "Title of the Duck Player card of the Set Up section in the home page")
static let newTabSetUpEmailProtectionCardTitle = NSLocalizedString("newTab.setup.email.protection.title", value: "Protect Your Inbox", comment: "Title of the Email Protection card of the Set Up section in the home page")

static let newTabSetUpDefaultBrowserAction = NSLocalizedString("newTab.setup.default.browser.action", value: "Make Default Browser", comment: "Action title on the action menu of the Default Browser card")
static let newTabSetUpDockAction = NSLocalizedString("newTab.setup.dock.action", value: "Keep In Dock", comment: "Action title on the action menu of the 'Add App to the Dock' card")
static let newTabSetUpDockConfirmation = NSLocalizedString("newTab.setup.dock.confirmation", value: "Added to Dock!", comment: "Confirmation title after user clicks on 'Add to Dock' card")
static let newTabSetUpImportAction = NSLocalizedString("newTab.setup.Import.action", value: "Import Now", comment: "Action title on the action menu of the Import card of the Set Up section in the home page")
static let newTabSetUpDuckPlayerAction = NSLocalizedString("newTab.setup.duck.player.action", value: "Try Duck Player", comment: "Action title on the action menu of the Duck Player card of the Set Up section in the home page")
static let newTabSetUpEmailProtectionAction = NSLocalizedString("newTab.setup.email.protection.action", value: "Get a Duck Address", comment: "Action title on the action menu of the Email Protection card of the Set Up section in the home page")
static let newTabSetUpRemoveItemAction = NSLocalizedString("newTab.setup.remove.item", value: "Dismiss", comment: "Action title on the action menu of the set up cards card of the SetUp section in the home page to remove the item")

static let newTabSetUpDefaultBrowserSummary = NSLocalizedString("newTab.setup.default.browser.summary", value: "We automatically block trackers as you browse. It's privacy, simplified.", comment: "Summary of the Default Browser card")
static let newTabSetUpDockSummary = NSLocalizedString("newTab.setup.dock.summary", value: "Get to DuckDuckGo faster by adding it to your Dock.", comment: "Summary of the 'Add App to the Dock' card")
static let newTabSetUpImportSummary = NSLocalizedString("newTab.setup.import.summary", value: "Import bookmarks, favorites, and passwords from your old browser.", comment: "Summary of the Import card of the Set Up section in the home page")
static let newTabSetUpDuckPlayerSummary = NSLocalizedString("newTab.setup.duck.player.summary", value: "Enjoy a clean viewing experience without personalized ads.", comment: "Summary of the Duck Player card of the Set Up section in the home page")
static let newTabSetUpEmailProtectionSummary = NSLocalizedString("newTab.setup.email.protection.summary", value: "Generate custom @duck.com addresses that clean trackers from incoming email.", comment: "Summary of the Email Protection card of the Set Up section in the home page")
Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public struct UserDefaultsWrapper<T> {
case homePageShowAllFavorites = "home.page.show.all.favorites"
case homePageShowAllFeatures = "home.page.show.all.features"
case homePageShowMakeDefault = "home.page.show.make.default"
case homePageShowAddToDock = "home.page.show.add.to.dock"
case homePageShowImport = "home.page.show.import"
case homePageShowDuckPlayer = "home.page.show.duck.player"
case homePageShowEmailProtection = "home.page.show.email.protection"
Expand Down
Loading

0 comments on commit 5f20c6c

Please sign in to comment.