Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding to the Dock automatically #2722

Merged
merged 31 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
17a469c
App added to the Dock when onboarding finishes
tomasstrba Mar 11, 2024
fa1136e
Delay restarting the Dock
tomasstrba Mar 11, 2024
c6c9e29
Merge branch 'main' into tom/add-to-the-dock
tomasstrba Apr 11, 2024
8f5b0da
Reliable way to add to the Dock programmatically
tomasstrba Apr 15, 2024
63980c9
New onboarding step implemented
tomasstrba Apr 17, 2024
3c3f7b7
Merge branch 'main' into tom/add-to-the-dock
tomasstrba Apr 22, 2024
e0ca297
New tap page card
tomasstrba Apr 22, 2024
250cb10
More sophisticated dock position based on existring browsers in the dock
tomasstrba Apr 29, 2024
0b730e1
Positioning based on the default browser
tomasstrba Apr 30, 2024
8e7b68b
Adding to the Dock disabled for the App Store build
tomasstrba Apr 30, 2024
6c5e4c5
Positioning next to Safari fixed
tomasstrba Apr 30, 2024
d2dbeb8
Option to add the application to the Dock from Settings
tomasstrba May 1, 2024
e348dfb
Pixels added
tomasstrba May 2, 2024
31a80c3
Merge branch 'main' into tom/add-to-the-dock
tomasstrba May 2, 2024
1de8d95
Unit Tests
tomasstrba May 2, 2024
57214f5
New pixel
tomasstrba May 2, 2024
b14f651
SwiftLint
tomasstrba May 2, 2024
123952e
Test failure fixed
tomasstrba May 2, 2024
cf9fa95
SwiftLint
tomasstrba May 3, 2024
93169a7
Confirmation when added to Dock from New Tab Page
tomasstrba May 3, 2024
f182d7a
Fix of sandbox-test-tool.xcscheme
tomasstrba May 7, 2024
dff9e4c
Changes after code review
tomasstrba May 7, 2024
6e14c9f
Merge branch 'main' into tom/add-to-the-dock
tomasstrba May 7, 2024
e29ed15
addToDockNewTabPageCardPresented pixel added
tomasstrba May 7, 2024
c6abe5f
Added to the Dock confirmation in onboarding
tomasstrba May 7, 2024
215c36c
Copy updated based on the Ship Review
tomasstrba May 15, 2024
8110811
add translations
SabrinaTardio May 20, 2024
92583a7
\n added
tomasstrba May 20, 2024
ae23af5
Unit tests corrected
tomasstrba May 20, 2024
f5c0e1f
Merge branch 'main' into tom/add-to-the-dock
tomasstrba May 20, 2024
7d8f43e
App Store unit tests corrected
tomasstrba May 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
10 changes: 10 additions & 0 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,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 @@ -773,11 +777,13 @@ 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. Would you like to keep DuckDuckGo in your dock so the browser is 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 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 @@ -1049,17 +1055,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: "Add App to the 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: "Add to 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 your 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: "Access 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 @@ -115,6 +115,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
Loading