├── .gitignore ├── KillControl.plist ├── Makefile ├── Package.swift ├── README.md ├── Sources ├── KillControl │ ├── KCAppResults.swift │ ├── KCHelper.swift │ └── Tweak.x.swift └── KillControlC │ ├── Tweak.m │ └── include │ ├── Tweak.h │ └── module.modulemap ├── control └── killcontrol ├── Makefile ├── Package.swift ├── Resources ├── Behaviour.plist ├── Info.plist ├── PrefIcon.png ├── PrefIcon@2x.png ├── PrefIcon@3x.png ├── Root.plist └── defaults.plist ├── Sources ├── killcontrol │ ├── KillControlBehaviourController.swift │ └── RootListController.swift └── killcontrolC │ ├── include │ ├── killcontrol.h │ └── module.modulemap │ └── killcontrol.m └── layout └── Library └── PreferenceLoader └── Preferences └── killcontrol.plist /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | 7 | .theos 8 | .swiftpm 9 | killcontrol/.theos 10 | killcontrol/.swiftpm 11 | /packages 12 | -------------------------------------------------------------------------------- /KillControl.plist: -------------------------------------------------------------------------------- 1 | { Filter = { Bundles = ( "com.apple.springboard" ); }; } 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ARCHS = arm64 arm64e 2 | THEOS_DEVICE_IP = root@localhost -p 2222 3 | INSTALL_TARGET_PROCESSES = SpringBoard 4 | TARGET = iphone:clang:14.4:14 5 | PACKAGE_VERSION = 1.2.2 6 | 7 | include $(THEOS)/makefiles/common.mk 8 | 9 | TWEAK_NAME = KillControl 10 | 11 | KillControl_PRIVATE_FRAMEWORKS = SpringBoard SpringBoardServices 12 | KillControl_FILES = $(shell find Sources/KillControl -name '*.swift') $(shell find Sources/KillControlC -name '*.m' -o -name '*.c' -o -name '*.mm' -o -name '*.cpp') 13 | KillControl_SWIFTFLAGS = -ISources/KillControlC/include 14 | KillControl_CFLAGS = -fobjc-arc -ISources/KillControlC/include 15 | 16 | include $(THEOS_MAKE_PATH)/tweak.mk 17 | SUBPROJECTS += killcontrol 18 | include $(THEOS_MAKE_PATH)/aggregate.mk 19 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import PackageDescription 4 | import Foundation 5 | 6 | let projectDir = URL(fileURLWithPath: #filePath).deletingLastPathComponent() 7 | 8 | @dynamicMemberLookup struct TheosConfiguration { 9 | private let dict: [String: String] 10 | init(at path: String) { 11 | let configURL = URL(fileURLWithPath: path, relativeTo: projectDir) 12 | guard let infoString = try? String(contentsOf: configURL) else { 13 | fatalError(""" 14 | Could not find Theos SPM config. Have you run `make spm` yet? 15 | """) 16 | } 17 | let pairs = infoString.split(separator: "\n").map { 18 | $0.split( 19 | separator: "=", maxSplits: 1, 20 | omittingEmptySubsequences: false 21 | ).map(String.init) 22 | }.map { ($0[0], $0[1]) } 23 | dict = Dictionary(uniqueKeysWithValues: pairs) 24 | } 25 | subscript( 26 | key: String, 27 | or defaultValue: @autoclosure () -> String? = nil 28 | ) -> String { 29 | if let value = dict[key] { 30 | return value 31 | } else if let def = defaultValue() { 32 | return def 33 | } else { 34 | fatalError(""" 35 | Could not get value of key '\(key)' from Theos SPM config. \ 36 | Try running `make spm` again. 37 | """) 38 | } 39 | } 40 | subscript(dynamicMember key: String) -> String { self[key] } 41 | } 42 | let conf = TheosConfiguration(at: ".theos/spm_config") 43 | 44 | let theosPath = conf.theos 45 | let sdk = conf.sdk 46 | let resourceDir = conf.swiftResourceDir 47 | let deploymentTarget = conf.deploymentTarget 48 | let triple = "arm64-apple-ios\(deploymentTarget)" 49 | 50 | let libFlags: [String] = [ 51 | "-F\(theosPath)/vendor/lib", "-F\(theosPath)/lib", 52 | "-I\(theosPath)/vendor/include", "-I\(theosPath)/include" 53 | ] 54 | 55 | let cFlags: [String] = libFlags + [ 56 | "-target", triple, "-isysroot", sdk, 57 | "-Wno-unused-command-line-argument", "-Qunused-arguments", 58 | ] 59 | 60 | let cxxFlags: [String] = [ 61 | ] 62 | 63 | let swiftFlags: [String] = libFlags + [ 64 | "-target", triple, "-sdk", sdk, "-resource-dir", resourceDir, 65 | ] 66 | 67 | let package = Package( 68 | name: "KillControl", 69 | platforms: [.iOS(deploymentTarget)], 70 | products: [ 71 | .library( 72 | name: "KillControl", 73 | targets: ["KillControl"] 74 | ), 75 | ], 76 | targets: [ 77 | .target( 78 | name: "KillControlC", 79 | cSettings: [.unsafeFlags(cFlags)], 80 | cxxSettings: [.unsafeFlags(cxxFlags)] 81 | ), 82 | .target( 83 | name: "KillControl", 84 | dependencies: ["KillControlC"], 85 | swiftSettings: [.unsafeFlags(swiftFlags)] 86 | ), 87 | ] 88 | ) 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KillControl 2 | An all in one solution to killing your apps. Swipe down to kill + more! 3 | -------------------------------------------------------------------------------- /Sources/KillControl/KCAppResults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KCAppResults.swift 3 | // 4 | // 5 | // Created by Noah Little on 17/7/2022. 6 | // 7 | 8 | import Orion 9 | import KillControlC 10 | 11 | @objc enum KCFilterType: Int { 12 | case noFilter = 0 13 | case whiteListed = 1 14 | case media = 2 15 | } 16 | 17 | final class KCAppResults: NSObject { 18 | 19 | public func identfierForItem(withLayout layout: SBAppLayout?) -> String? { 20 | //Returns the bundle identifier of an app card. 21 | 22 | guard let layout = layout else { 23 | return nil 24 | } 25 | 26 | let displayItem = Ivars(layout.protobufRepresentation())._primaryDisplayItem 27 | let identifier: String = Ivars(displayItem)._bundleIdentifier as String 28 | return identifier 29 | } 30 | 31 | public func filterTypeForItem(withIdentifier identifier: String) -> KCFilterType { 32 | //Returns true if app is whitelisted. 33 | 34 | //Check and return if app is media app. 35 | if localSettings.excludeMediaApps { 36 | if let nowPlayingApp = SBMediaController.sharedInstance().nowPlayingApplication() { 37 | if nowPlayingApp.bundleIdentifier == identifier && !SBMediaController.sharedInstance().isPaused() { 38 | return .media 39 | } 40 | } 41 | } 42 | 43 | //Check and return if app is white listed. 44 | if localSettings.whitelistApps.contains(identifier) { 45 | return .whiteListed 46 | } 47 | 48 | //App has no filter, return with no filter. 49 | return .noFilter 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/KillControl/KCHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KCHelper.swift 3 | // 4 | // 5 | // Created by Noah Little on 18/7/2022. 6 | // 7 | 8 | import KillControlC 9 | 10 | final class KCHelper: NSObject { 11 | static let sharedInstance = KCHelper() 12 | 13 | private var timer: Timer? 14 | 15 | private var switcher: SBMainSwitcherViewController! 16 | 17 | public func killUsingTimer(withInterval interval: Double, controller switcher: SBMainSwitcherViewController) { 18 | 19 | self.switcher = switcher 20 | 21 | if timer == nil { 22 | timer = Timer() 23 | } 24 | 25 | timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false, block: { action in 26 | localSettings.comingFromTimer = true 27 | 28 | self.killApps(localSettings.onlyKillChosenWhenLocked ? localSettings.onlyKillTheseWhenLocked : nil, controller: nil) 29 | 30 | self.timer?.invalidate() 31 | }) 32 | } 33 | 34 | private func filteredLayouts(withFilter bundleIDS: [String]?) -> [SBAppLayout] { 35 | //Returns a list of apps to be killed 36 | 37 | /* Create two sets, one that contains all app layouts that are currently in the app switcher, 38 | and another empty which we will add excluded apps to. */ 39 | var appLayouts: Set = Set(switcher.appLayouts(forSwitcherContentController: switcher)) 40 | var layoutsToExclude = Set() 41 | 42 | //Adding exlcuded apps to our layoutsToExclude set. 43 | for app in appLayouts { 44 | //Get the bundleIdentifier from each app's layout. 45 | guard let identifier = KCAppResults().identfierForItem(withLayout: app) else { 46 | continue 47 | } 48 | 49 | //Skip the media app if it's playing, device is locked and is in the onlyKillTheseWhenLocked array. 50 | if localSettings.deviceLocked && localSettings.onlyKillChosenWhenLocked && localSettings.onlyKillTheseWhenLocked.contains(identifier) { 51 | guard KCAppResults().filterTypeForItem(withIdentifier: identifier) != .media else { 52 | layoutsToExclude.insert(app) 53 | continue 54 | } 55 | } 56 | 57 | //Exclude if now playing. 58 | if localSettings.excludeMediaApps { 59 | guard KCAppResults().filterTypeForItem(withIdentifier: identifier) != .media else { 60 | layoutsToExclude.insert(app) 61 | continue 62 | } 63 | } 64 | 65 | if !(localSettings.onlyKillChosenWhenLocked && localSettings.deviceLocked) { 66 | //Exclude if black listed 67 | guard !localSettings.whitelistApps.contains(identifier) else { 68 | layoutsToExclude.insert(app) 69 | continue 70 | } 71 | } 72 | 73 | //Exclude if not found in the bundleIDS argument (If that exists). 74 | if bundleIDS != nil { 75 | guard bundleIDS!.contains(identifier) else { 76 | layoutsToExclude.insert(app) 77 | continue 78 | } 79 | } 80 | } 81 | 82 | //Subtract the excluded app set from the set that contains all app layouts, then return it as an array. 83 | appLayouts = appLayouts.subtracting(layoutsToExclude) 84 | return Array(appLayouts) 85 | } 86 | 87 | public func killApps(_ apps: [String]?, controller switcher: SBMainSwitcherViewController?) { 88 | 89 | if switcher != nil { 90 | self.switcher = switcher 91 | } 92 | 93 | //Check if this method was called from a timer. If it was, only continue if the device is locked. 94 | if localSettings.hasGracePeriod && localSettings.comingFromTimer { 95 | localSettings.comingFromTimer = false 96 | 97 | guard localSettings.deviceLocked else { 98 | return 99 | } 100 | } 101 | 102 | //Get a filtered app layout list that respects the user's blacklisting settings and open/kill rules. 103 | let layoutList = filteredLayouts(withFilter: apps) 104 | 105 | //Skip the confirmation prompt if the user doesn't want it. 106 | guard localSettings.askBeforeKilling && !localSettings.deviceLocked else { 107 | killInLayoutList(layoutList) 108 | return 109 | } 110 | 111 | //Present confirmation alert 112 | let alert = UIAlertController(title: "KillControl", message: "Are you sure you want to kill all apps?", preferredStyle: .alert) 113 | 114 | alert.addAction(UIAlertAction(title: "Kill", style: .destructive, handler: { action in 115 | self.killInLayoutList(layoutList) 116 | })) 117 | 118 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) 119 | 120 | self.switcher.present(alert, animated: true) 121 | } 122 | 123 | //orion: new 124 | private func killInLayoutList(_ list: [SBAppLayout]) { 125 | //Play haptic feedback when all apps are killed (Only when the device is unlocked). 126 | if !localSettings.deviceLocked { 127 | let thud = UIImpactFeedbackGenerator(style: .medium) 128 | thud.prepare() 129 | thud.impactOccurred() 130 | } 131 | 132 | //Kill all apps. 133 | for layout in list { 134 | switcher._remove(layout, forReason: 0) 135 | } 136 | } 137 | 138 | public func invalidateTimer() { 139 | //Invalidate the timer. 140 | 141 | if let timer = timer { 142 | if timer.isValid { 143 | timer.invalidate() 144 | } 145 | } 146 | } 147 | 148 | private override init() {} 149 | } 150 | -------------------------------------------------------------------------------- /Sources/KillControl/Tweak.x.swift: -------------------------------------------------------------------------------- 1 | import Orion 2 | import KillControlC 3 | 4 | //MARK: - Preferences storage 5 | struct localSettings { 6 | static var isEnabled: Bool! 7 | //Killing apps after lock + grace period 8 | static var killAfterLock: Bool! 9 | static var hasGracePeriod: Bool! 10 | static var killLockGracePeriodTimeMeasurement: Int! //1: Sec, 2: Min, 3: Hour 11 | static var killLockGracePeriod: Double! 12 | static var onlyKillChosenWhenLocked: Bool! 13 | static var onlyKillTheseWhenLocked: [String]! 14 | static var killAfterForcedlLock: Bool! 15 | //Kill/Open rule 16 | static var killOnAppLaunch: Bool! 17 | static var openOnAppLaunch: Bool! 18 | static var killTheseAppsOne: [String]! 19 | static var whenThisAppOpensOne: String! 20 | static var openTheseAppsTwo: [String]! 21 | static var whenThisAppOpensTwo: String! 22 | //Blacklisting 23 | static var whitelistApps: Set! 24 | static var excludeMediaApps: Bool! 25 | static var useWhiteListGesture: Bool! 26 | static var whiteListShortcut: Int! //1 = One finger hold, 2 = Two finger hold 27 | //Misc 28 | static var swipeDownToKillAll: Bool! 29 | static var preventSwipe: Bool! 30 | static var askBeforeKilling: Bool! 31 | //Timer settings 32 | static var comingFromTimer = false 33 | static var deviceLocked: Bool { 34 | return SBLockStateAggregator.sharedInstance().lockState() == 2 || SBLockStateAggregator.sharedInstance().lockState() == 3 35 | } 36 | static var time: Double { 37 | switch localSettings.killLockGracePeriodTimeMeasurement { 38 | case 1: 39 | return localSettings.killLockGracePeriod //Seconds 40 | case 2: 41 | return localSettings.killLockGracePeriod * 60 //Minutes 42 | case 3: 43 | return localSettings.killLockGracePeriod * 60 * 60 //Hours 44 | default: 45 | return localSettings.killLockGracePeriod //Seconds 46 | } 47 | } 48 | } 49 | 50 | //MARK: - Hook groups 51 | struct tweak: HookGroup {} 52 | struct killAfterLock: HookGroup {} 53 | struct excludeMediaApps: HookGroup {} 54 | struct killAfterForcedlLock: HookGroup {} 55 | 56 | //MARK: - Swipe down to kill, white-list shortcut. 57 | class SBReusableSnapshotItemContainer_Hook: ClassHook { 58 | typealias Group = tweak 59 | 60 | func initWithFrame(_ frame: CGRect, appLayout layout: SBAppLayout, delegate arg3: AnyObject, active arg4: Bool) -> Target { 61 | 62 | /* Add long hold gesture to each switcher app card, which will be used as a shortcut to whitelist / un-whitelist apps. */ 63 | 64 | if localSettings.excludeMediaApps { 65 | NotificationCenter.default.addObserver(target, 66 | selector: #selector(refreshLockedStatus), 67 | name: NSNotification.Name("KillControl.refreshLock"), 68 | object: nil) 69 | } 70 | 71 | if localSettings.useWhiteListGesture { 72 | let longHold = UILongPressGestureRecognizer(target: target, action: #selector(killControlToggleItemWhiteListedWithRecogniser(_:))) 73 | longHold.minimumPressDuration = 1.0 74 | longHold.numberOfTouchesRequired = localSettings.whiteListShortcut == 1 ? 1 : 2 75 | target.addGestureRecognizer(longHold) 76 | } 77 | 78 | return orig.initWithFrame(frame, appLayout: layout, delegate: arg3, active: arg4) 79 | } 80 | 81 | func _updateTransformForCurrentHighlight() { 82 | orig._updateTransformForCurrentHighlight() 83 | 84 | //Swipe down to kill all gesture. 85 | guard localSettings.swipeDownToKillAll else { 86 | return 87 | } 88 | 89 | //Kills all apps when the kill progress is <= -0.2. 90 | if target.killingProgress <= -0.2 { 91 | guard let switcher = SBMainSwitcherViewController.sharedInstanceIfExists() else { 92 | return 93 | } 94 | 95 | KCHelper.sharedInstance.killApps(nil, controller: switcher) 96 | } 97 | } 98 | 99 | func _scrollViewShouldPanGestureTryToBegin(_ arg1: AnyObject) -> Bool { 100 | /* Prevent swiping gestures on white-listed apps if the user has 101 | that option enabled. */ 102 | 103 | guard localSettings.preventSwipe else { 104 | return orig._scrollViewShouldPanGestureTryToBegin(arg1) 105 | } 106 | 107 | //Prevent swipe gesture if app id is white-listed / now playing. 108 | if let identifier = KCAppResults().identfierForItem(withLayout: Ivars(target)._appLayout) { 109 | let filter = KCAppResults().filterTypeForItem(withIdentifier: identifier) 110 | guard filter == .noFilter else { 111 | return false 112 | } 113 | } 114 | 115 | return orig._scrollViewShouldPanGestureTryToBegin(arg1) 116 | } 117 | 118 | func _updateHeaderAnimated(_ animated: Bool) { 119 | orig._updateHeaderAnimated(animated) 120 | 121 | //Refresh locked status when presenting the app switcher. 122 | DispatchQueue.main.async { 123 | self.refreshLockedStatus() 124 | } 125 | } 126 | 127 | func setTitleOpacity(_ opacity: CGFloat) { 128 | orig.setTitleOpacity(opacity) 129 | 130 | /* Apply header view's title opacity to our lock icon if the app is white listed. 131 | Exit if app is not white listed. */ 132 | 133 | if let identifier = KCAppResults().identfierForItem(withLayout: Ivars(target)._appLayout) { 134 | let filter = KCAppResults().filterTypeForItem(withIdentifier: identifier) 135 | guard filter != .noFilter else { 136 | return 137 | } 138 | } 139 | 140 | guard let headerView = Ivars(target)._iconAndLabelHeader else { 141 | return 142 | } 143 | 144 | if let lockView = headerView.subviews.first(where: { $0 is UIImageView }) { 145 | lockView.alpha = opacity 146 | } 147 | } 148 | 149 | //orion: new 150 | @objc func refreshLockedStatus() { 151 | //Refreshes locked status. This method is called when the app switcher gets presented. 152 | if let identifier = KCAppResults().identfierForItem(withLayout: Ivars(target)._appLayout) { 153 | let filter = KCAppResults().filterTypeForItem(withIdentifier: identifier) 154 | killControlUpdateHeaderItems(withFilter: filter) 155 | } 156 | } 157 | 158 | //orion: new 159 | @objc func killControlToggleItemWhiteListedWithRecogniser(_ recogniser: UILongPressGestureRecognizer) { 160 | //Request toggling white-listed for an app by holding your finger for n seconds on an app. 161 | if recogniser.state == .began { 162 | if let identifier = KCAppResults().identfierForItem(withLayout: Ivars(target)._appLayout) { 163 | killControlToggleItemWhiteListed(withIdentifer: identifier) 164 | } 165 | } 166 | } 167 | 168 | //orion: new 169 | func killControlToggleItemWhiteListed(withIdentifer identifier: String) { 170 | //Toggle white-listed for an app. 171 | 172 | var filterType = KCAppResults().filterTypeForItem(withIdentifier: identifier) 173 | 174 | switch filterType { 175 | case .noFilter: 176 | //Add to whitelisted app list if doesn't exist. 177 | localSettings.whitelistApps.insert(identifier) 178 | filterType = .whiteListed 179 | break 180 | case .whiteListed: 181 | //Remove from whitelisted app list if exists. 182 | localSettings.whitelistApps.remove(identifier) 183 | filterType = .noFilter 184 | break 185 | case .media: 186 | //Don't continue if app is a media app. 187 | return 188 | } 189 | 190 | killControlUpdateHeaderItems(withFilter: filterType) 191 | pushChangesToWhiteList() 192 | 193 | //Haptics 194 | let thud = UIImpactFeedbackGenerator(style: .medium) 195 | thud.prepare() 196 | thud.impactOccurred() 197 | } 198 | 199 | //orion: new 200 | func killControlUpdateHeaderItems(withFilter filter: KCFilterType) { 201 | //This method is used to add/remove the lock to the header view of an app card. 202 | 203 | guard let headerView = Ivars(target)._iconAndLabelHeader else { 204 | return 205 | } 206 | 207 | //Remove lock if not white-listed. 208 | if filter == .noFilter { 209 | if let lockView = headerView.subviews.first(where: { $0 is UIImageView }) { 210 | lockView.removeFromSuperview() 211 | } 212 | return 213 | } 214 | 215 | //Don't add lock if it already exists. 216 | guard !headerView.subviews.contains(where: { $0 is UIImageView }) else { 217 | return 218 | } 219 | 220 | //Proceed to add lock if white-listed. 221 | let appLabel = Ivars(headerView)._firstTitleLabel 222 | let spacing = Ivars(headerView)._spacingBetweenSnapshotAndIcon 223 | 224 | let image = UIImage(systemName: filter == .media ? "music.note" : "lock.fill") 225 | 226 | let lockImageView = UIImageView(image: image) 227 | lockImageView.translatesAutoresizingMaskIntoConstraints = false 228 | lockImageView.tintColor = .white 229 | lockImageView.contentMode = .scaleAspectFit 230 | headerView.addSubview(lockImageView) 231 | //Constraints 232 | lockImageView.leftAnchor.constraint(equalTo: appLabel.rightAnchor, constant: spacing).isActive = true 233 | lockImageView.centerYAnchor.constraint(equalTo: appLabel.centerYAnchor).isActive = true 234 | lockImageView.heightAnchor.constraint(equalTo: appLabel.heightAnchor).isActive = true 235 | lockImageView.widthAnchor.constraint(equalTo: appLabel.heightAnchor).isActive = true 236 | } 237 | } 238 | 239 | class FBProcessManagerHook: ClassHook { 240 | typealias Group = tweak 241 | 242 | func _createProcessWithExecutionContext(_ executionContext: FBProcessExecutionContext) -> FBProcess? { 243 | let proccess = orig._createProcessWithExecutionContext(executionContext) 244 | 245 | //Opens user-specified apps in the background when a different app is opened. 246 | if localSettings.openOnAppLaunch { 247 | if let processIdentifier = proccess?.identity.embeddedApplicationIdentifier { 248 | if processIdentifier == localSettings.whenThisAppOpensTwo { 249 | DispatchQueue.main.async { 250 | for identifier in localSettings.openTheseAppsTwo { 251 | DispatchQueue.global().async { 252 | //Launch the application in the background 253 | SBSLaunchApplicationWithIdentifier(identifier as CFString, true) 254 | } 255 | 256 | //Manually add the app to switcher 257 | let displayItem = SBDisplayItem(type: 0, 258 | bundleIdentifier: identifier, 259 | uniqueIdentifier: "sceneID:\(identifier)-default") 260 | 261 | SBMainSwitcherViewController.sharedInstanceIfExists().addAppLayout(forDisplayItem: displayItem, completion: nil) 262 | } 263 | } 264 | } 265 | } 266 | } 267 | 268 | //Kills user-specified apps when a different app is opened. 269 | if localSettings.killOnAppLaunch { 270 | if let processIdentifier = proccess?.identity.embeddedApplicationIdentifier { 271 | if processIdentifier == localSettings.whenThisAppOpensOne { 272 | DispatchQueue.main.async { 273 | //Kill user-specified apps 274 | guard let switcher = SBMainSwitcherViewController.sharedInstanceIfExists() else { 275 | return 276 | } 277 | 278 | KCHelper.sharedInstance.killApps(localSettings.killTheseAppsOne, controller: switcher) 279 | } 280 | } 281 | } 282 | } 283 | 284 | return proccess 285 | } 286 | 287 | } 288 | 289 | class SBLockStateAggregator_Hook: ClassHook { 290 | typealias Group = killAfterLock 291 | 292 | func _updateLockState() { 293 | orig._updateLockState() 294 | 295 | /* Lock states 296 | 0 = Device unlocked and lock screen dismissed. 297 | 1 = Device unlocked and lock screen not dismissed. 298 | 2 = Locking... ?? 299 | 3 = Locked. 300 | */ 301 | 302 | guard let switcher = SBMainSwitcherViewController.sharedInstanceIfExists() else { 303 | //Don't progress if switcher has no shared instance ready for use 304 | return 305 | } 306 | 307 | guard !switcher.appLayouts(forSwitcherContentController: switcher).isEmpty else { 308 | //Don't progress if app switcher empty 309 | return 310 | } 311 | 312 | guard localSettings.deviceLocked else { 313 | //Invalidate the timer if it is valid and the device is unlocked 314 | KCHelper.sharedInstance.invalidateTimer() 315 | //Don't progress if unlocked 316 | return 317 | } 318 | 319 | guard !localSettings.killAfterForcedlLock else { 320 | //Don't progress if lock button only is enabled. 321 | return 322 | } 323 | 324 | if localSettings.hasGracePeriod { 325 | KCHelper.sharedInstance.killUsingTimer(withInterval: localSettings.time, controller: switcher) 326 | } else { 327 | KCHelper.sharedInstance.killApps(localSettings.onlyKillChosenWhenLocked ? localSettings.onlyKillTheseWhenLocked : nil, controller: switcher) 328 | } 329 | } 330 | } 331 | 332 | class SBDashBoardLockScreenEnvironment_Hook: ClassHook { 333 | typealias Group = killAfterForcedlLock 334 | 335 | @Property (.nonatomic, .retain) var lastLockPressTime: Date? = Date() 336 | 337 | func handleLockButtonPress() -> Bool { 338 | lastLockPressTime = Date() 339 | return orig.handleLockButtonPress() 340 | } 341 | 342 | func prepareForUILock() { 343 | orig.prepareForUILock() 344 | 345 | guard let lastLockPressTime = lastLockPressTime else { 346 | return 347 | } 348 | 349 | let interval = DateInterval(start: lastLockPressTime, end: Date()) 350 | 351 | if interval.duration <= 2 { 352 | guard let switcher = SBMainSwitcherViewController.sharedInstanceIfExists() else { 353 | //Don't progress if switcher has no shared instance ready for use 354 | return 355 | } 356 | 357 | if localSettings.hasGracePeriod { 358 | KCHelper.sharedInstance.killUsingTimer(withInterval: localSettings.time, controller: switcher) 359 | } else { 360 | KCHelper.sharedInstance.killApps(localSettings.onlyKillChosenWhenLocked ? localSettings.onlyKillTheseWhenLocked : nil, controller: switcher) 361 | } 362 | } 363 | } 364 | } 365 | 366 | class SBMediaController_Hook: ClassHook { 367 | typealias Group = excludeMediaApps 368 | 369 | func _mediaRemoteNowPlayingApplicationIsPlayingDidChange(_ change: AnyObject) { 370 | orig._mediaRemoteNowPlayingApplicationIsPlayingDidChange(change) 371 | 372 | //Update the now playing indicator when media playback changes. 373 | NotificationCenter.default.post(name: NSNotification.Name("KillControl.refreshLock"), 374 | object: nil, 375 | userInfo: nil) 376 | } 377 | } 378 | 379 | fileprivate func pushChangesToWhiteList() { 380 | let path = "/var/mobile/Library/Preferences/com.ginsu.killcontrol.plist" 381 | 382 | guard let dict = NSDictionary(contentsOfFile: path) else { 383 | return 384 | } 385 | 386 | let newWhitelist = Array(localSettings.whitelistApps) 387 | dict.setValue(newWhitelist, forKey: "whitelistApps") 388 | 389 | dict.write(toFile: path, atomically: true) 390 | } 391 | 392 | //MARK: - Preferences 393 | fileprivate func readPrefs() { 394 | 395 | let path = "/var/mobile/Library/Preferences/com.ginsu.killcontrol.plist" 396 | 397 | if !FileManager().fileExists(atPath: path) { 398 | try? FileManager().copyItem(atPath: "Library/PreferenceBundles/killcontrol.bundle/defaults.plist", toPath: path) 399 | } 400 | 401 | guard let dict = NSDictionary(contentsOfFile: path) else { 402 | return 403 | } 404 | 405 | //Reading values 406 | localSettings.isEnabled = dict.value(forKey: "isEnabled") as? Bool ?? true 407 | //Kill when locked 408 | localSettings.killAfterLock = dict.value(forKey: "killAfterLock") as? Bool ?? false 409 | localSettings.killAfterForcedlLock = dict.value(forKey: "killAfterForcedlLock") as? Bool ?? false 410 | localSettings.hasGracePeriod = dict.value(forKey: "hasGracePeriod") as? Bool ?? false 411 | localSettings.killLockGracePeriodTimeMeasurement = dict.value(forKey: "killLockGracePeriodTimeMeasurement") as? Int ?? 1 412 | localSettings.killLockGracePeriod = dict.value(forKey: "killLockGracePeriod") as? Double ?? 10.0 413 | localSettings.onlyKillChosenWhenLocked = dict.value(forKey: "onlyKillChosenWhenLocked") as? Bool ?? false 414 | localSettings.onlyKillTheseWhenLocked = dict.value(forKey: "onlyKillTheseWhenLocked") as? [String] ?? [String]() 415 | //Kill/Open Rules 416 | localSettings.killOnAppLaunch = dict.value(forKey: "killOnAppLaunch") as? Bool ?? false 417 | localSettings.openOnAppLaunch = dict.value(forKey: "openOnAppLaunch") as? Bool ?? false 418 | localSettings.killTheseAppsOne = dict.value(forKey: "killTheseAppsOne") as? [String] ?? [""] 419 | localSettings.whenThisAppOpensOne = dict.value(forKey: "whenThisAppOpensOne") as? String ?? "" 420 | localSettings.openTheseAppsTwo = dict.value(forKey: "openTheseAppsTwo") as? [String] ?? [""] 421 | localSettings.whenThisAppOpensTwo = dict.value(forKey: "whenThisAppOpensTwo") as? String ?? "" 422 | //Blacklisting 423 | localSettings.whitelistApps = Set(dict.value(forKey: "whitelistApps") as? [String] ?? [""]) 424 | localSettings.excludeMediaApps = dict.value(forKey: "excludeMediaApps") as? Bool ?? true 425 | localSettings.useWhiteListGesture = dict.value(forKey: "useWhiteListGesture") as? Bool ?? true 426 | localSettings.whiteListShortcut = dict.value(forKey: "whiteListShortcut") as? Int ?? 1 427 | //Misc 428 | localSettings.swipeDownToKillAll = dict.value(forKey: "swipeDownToKillAll") as? Bool ?? true 429 | localSettings.askBeforeKilling = dict.value(forKey: "askBeforeKilling") as? Bool ?? false 430 | localSettings.preventSwipe = dict.value(forKey: "preventSwipe") as? Bool ?? false 431 | } 432 | 433 | struct KillControl: Tweak { 434 | init() { 435 | readPrefs() 436 | if (localSettings.isEnabled) { 437 | tweak().activate() 438 | 439 | if localSettings.excludeMediaApps { 440 | excludeMediaApps().activate() 441 | } 442 | 443 | if localSettings.killAfterLock { 444 | killAfterLock().activate() 445 | 446 | if localSettings.killAfterForcedlLock { 447 | killAfterForcedlLock().activate() 448 | } 449 | } 450 | } 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /Sources/KillControlC/Tweak.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | __attribute__((constructor)) static void init() { 4 | // Initialize Orion - do not remove this line. 5 | orion_init(); 6 | // Custom initialization code goes here. 7 | } 8 | -------------------------------------------------------------------------------- /Sources/KillControlC/include/Tweak.h: -------------------------------------------------------------------------------- 1 | #include 2 | #import 3 | 4 | #ifndef SPRINGBOARDSERVICES_H_ 5 | extern int SBSLaunchApplicationWithIdentifier(CFStringRef identifier, Boolean suspended); 6 | #endif 7 | 8 | @interface SBPBAppLayout : NSObject 9 | @end 10 | 11 | @interface SBPBDisplayItem : NSObject 12 | @end 13 | 14 | @interface SBAppLayout : NSObject 15 | - (SBPBAppLayout *)protobufRepresentation; 16 | @end 17 | 18 | @interface SBFluidSwitcherIconImageContainerView : UIView 19 | @end 20 | 21 | @interface SBFluidSwitcherItemContainerHeaderView : UIView 22 | @end 23 | 24 | @interface SBReusableSnapshotItemContainer : UIView 25 | @property (nonatomic,copy) NSArray * headerItems; 26 | @property (nonatomic,readonly) double killingProgress; 27 | - (id)initWithFrame:(CGRect)arg1 appLayout:(id)arg2 delegate:(id)arg3 active:(BOOL)arg4 ; 28 | - (void)setHeaderItems:(id)arg1 animated:(BOOL)arg2; 29 | @end 30 | 31 | @interface SBMainSwitcherViewController : UIViewController 32 | + (instancetype)sharedInstanceIfExists; 33 | - (void)_removeAppLayout:(SBAppLayout *)layout forReason:(NSInteger)reason; 34 | - (void)addAppLayoutForDisplayItem:(id)arg1 completion:(/*^block*/id)arg2 ; 35 | - (NSArray*)appLayoutsForSwitcherContentController:(id)arg1; 36 | - (void)fluidSwitcherGestureManager:(id)arg1 willEndDraggingWindowWithSceneIdentifier:(id)arg2 ; 37 | - (BOOL)switcherContentController:(id)arg1 supportsKillingOfAppLayout:(id)arg2 ; 38 | //New methods: 39 | - (void)killControlKillApps:(NSArray*)apps; 40 | @end 41 | 42 | @interface SBLockStateAggregator : NSObject 43 | + (instancetype)sharedInstance; 44 | - (unsigned long long)lockState; 45 | @end 46 | 47 | @interface SBDashBoardLockScreenEnvironment : NSObject 48 | @end 49 | 50 | @interface SBAssistantRootViewController : UIViewController 51 | @end 52 | 53 | @interface RBSProcessIdentity : NSObject 54 | @property(readonly, copy, nonatomic) NSString *executablePath; 55 | @property(readonly, copy, nonatomic) NSString *embeddedApplicationIdentifier; 56 | @end 57 | 58 | @interface FBProcess : NSObject 59 | @property (nonatomic,readonly) RBSProcessIdentity * identity; 60 | @end 61 | 62 | @interface FBProcessExecutionContext : NSObject 63 | @property (nonatomic,copy) RBSProcessIdentity* identity; 64 | @end 65 | 66 | @interface FBProcessManager : NSObject 67 | @end 68 | 69 | @interface SBDisplayItem : NSObject 70 | + (instancetype)displayItemWithType:(long long)arg1 bundleIdentifier:(id)arg2 uniqueIdentifier:(id)arg3; 71 | + (instancetype)applicationDisplayItemWithBundleIdentifier:(id)arg1 sceneIdentifier:(id)arg2; 72 | @end 73 | 74 | @interface SBApplication : NSObject 75 | @property (nonatomic,readonly) NSString * bundleIdentifier; 76 | @end 77 | 78 | @interface SBMediaController : NSObject 79 | + (instancetype)sharedInstance; 80 | - (SBApplication *)nowPlayingApplication; 81 | - (BOOL)isPaused; 82 | @end 83 | -------------------------------------------------------------------------------- /Sources/KillControlC/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module KillControlC { 2 | umbrella "." 3 | export * 4 | } 5 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Package: com.ginsu.killcontrol 2 | Name: KillControl 3 | Version: 0.0.1 4 | Architecture: iphoneos-arm 5 | Description: An awesome Orion tweak! 6 | Maintainer: Ginsu 7 | Author: Ginsu 8 | Section: Tweaks 9 | Depends: dev.theos.orion14, firmware (>= 14.0), com.ginsu.libgscommon, com.opa334.altlist, preferenceloader 10 | -------------------------------------------------------------------------------- /killcontrol/Makefile: -------------------------------------------------------------------------------- 1 | ARCHS = arm64 arm64e 2 | TARGET = iphone:clang:14.4:14 3 | 4 | include $(THEOS)/makefiles/common.mk 5 | 6 | BUNDLE_NAME = killcontrol 7 | 8 | killcontrol_LIBRARIES = gscommon 9 | killcontrol_EXTRA_FRAMEWORKS = AltList 10 | killcontrol_FILES = $(shell find Sources/killcontrol -name '*.swift') $(shell find Sources/killcontrolC -name '*.m' -o -name '*.c' -o -name '*.mm' -o -name '*.cpp') 11 | killcontrol_INSTALL_PATH = /Library/PreferenceBundles 12 | killcontrol_SWIFTFLAGS = -ISources/killcontrolC/include 13 | killcontrol_CFLAGS = -fobjc-arc 14 | 15 | include $(THEOS_MAKE_PATH)/bundle.mk 16 | -------------------------------------------------------------------------------- /killcontrol/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import PackageDescription 4 | import Foundation 5 | 6 | let projectDir = URL(fileURLWithPath: #filePath).deletingLastPathComponent() 7 | 8 | @dynamicMemberLookup struct TheosConfiguration { 9 | private let dict: [String: String] 10 | init(at path: String) { 11 | let configURL = URL(fileURLWithPath: path, relativeTo: projectDir) 12 | guard let infoString = try? String(contentsOf: configURL) else { 13 | fatalError(""" 14 | Could not find Theos SPM config. Have you run `make spm` yet? 15 | """) 16 | } 17 | let pairs = infoString.split(separator: "\n").map { 18 | $0.split( 19 | separator: "=", maxSplits: 1, 20 | omittingEmptySubsequences: false 21 | ).map(String.init) 22 | }.map { ($0[0], $0[1]) } 23 | dict = Dictionary(uniqueKeysWithValues: pairs) 24 | } 25 | subscript( 26 | key: String, 27 | or defaultValue: @autoclosure () -> String? = nil 28 | ) -> String { 29 | if let value = dict[key] { 30 | return value 31 | } else if let def = defaultValue() { 32 | return def 33 | } else { 34 | fatalError(""" 35 | Could not get value of key '\(key)' from Theos SPM config. \ 36 | Try running `make spm` again. 37 | """) 38 | } 39 | } 40 | subscript(dynamicMember key: String) -> String { self[key] } 41 | } 42 | let conf = TheosConfiguration(at: ".theos/spm_config") 43 | 44 | let theosPath = conf.theos 45 | let sdk = conf.sdk 46 | let resourceDir = conf.swiftResourceDir 47 | let deploymentTarget = conf.deploymentTarget 48 | let triple = "arm64-apple-ios\(deploymentTarget)" 49 | 50 | let libFlags: [String] = [ 51 | "-F\(theosPath)/vendor/lib", "-F\(theosPath)/lib", 52 | "-I\(theosPath)/vendor/include", "-I\(theosPath)/include" 53 | ] 54 | 55 | let cFlags: [String] = libFlags + [ 56 | "-target", triple, "-isysroot", sdk, 57 | "-Wno-unused-command-line-argument", "-Qunused-arguments", 58 | ] 59 | 60 | let cxxFlags: [String] = [ 61 | ] 62 | 63 | let swiftFlags: [String] = libFlags + [ 64 | "-target", triple, "-sdk", sdk, "-resource-dir", resourceDir, 65 | ] 66 | 67 | let package = Package( 68 | name: "killcontrol", 69 | platforms: [.iOS("12.2")], 70 | products: [ 71 | .library( 72 | name: "killcontrol", 73 | targets: ["killcontrol"] 74 | ), 75 | ], 76 | targets: [ 77 | .target( 78 | name: "killcontrolC", 79 | cSettings: [.unsafeFlags(cFlags)], 80 | cxxSettings: [.unsafeFlags(cxxFlags)] 81 | ), 82 | .target( 83 | name: "killcontrol", 84 | dependencies: ["killcontrolC"], 85 | swiftSettings: [.unsafeFlags(swiftFlags)] 86 | ), 87 | ] 88 | ) 89 | -------------------------------------------------------------------------------- /killcontrol/Resources/Behaviour.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | items 6 | 7 | 8 | cell 9 | PSGroupCell 10 | label 11 | Gestures 12 | footerText 13 | Swipe down on an app to kill all apps instantly. 14 | 15 | 16 | cell 17 | PSSwitchCell 18 | cellClass 19 | PSSubtitleSwitchTableCell 20 | cellSubtitleText 21 | Kill all apps when swiping down 22 | default 23 | 24 | defaults 25 | com.ginsu.killcontrol 26 | key 27 | swipeDownToKillAll 28 | label 29 | Swipe down to kill all 30 | 31 | 32 | cell 33 | PSGroupCell 34 | label 35 | Device lock 36 | footerText 37 | Kill all of your apps after locking the device. You can also add a grace period in which the apps won't be killed. 38 | 39 | 40 | cell 41 | PSSwitchCell 42 | cellClass 43 | PSSubtitleSwitchTableCell 44 | cellSubtitleText 45 | Kill apps when locking the device. 46 | default 47 | 48 | defaults 49 | com.ginsu.killcontrol 50 | key 51 | killAfterLock 52 | label 53 | Kill after lock 54 | id 55 | killAfterLockStatus 56 | 57 | 58 | cell 59 | PSSwitchCell 60 | cellClass 61 | PSSubtitleSwitchTableCell 62 | cellSubtitleText 63 | Only kill chosen apps when locked 64 | default 65 | 66 | defaults 67 | com.ginsu.killcontrol 68 | key 69 | onlyKillChosenWhenLocked 70 | label 71 | Black list 72 | dynamicRule 73 | killAfterLockStatus,0 74 | id 75 | onlyKillChosenWhenLockedStatus 76 | 77 | 78 | bundle 79 | AltList 80 | cell 81 | PSLinkListCell 82 | detail 83 | ATLApplicationListMultiSelectionController 84 | key 85 | onlyKillTheseWhenLocked 86 | defaults 87 | com.ginsu.killcontrol 88 | sections 89 | 90 | 91 | sectionType 92 | Visible 93 | 94 | 95 | isController 96 | 97 | overridePrincipalClass 98 | 99 | label 100 | Only kill these apps 101 | useSearchBar 102 | 103 | alphabeticIndexingEnabled 104 | 105 | dynamicRule 106 | onlyKillChosenWhenLockedStatus,0 107 | 108 | 109 | cell 110 | PSSwitchCell 111 | cellClass 112 | PSSubtitleSwitchTableCell 113 | cellSubtitleText 114 | Don't kill apps with auto-lock 115 | default 116 | 117 | defaults 118 | com.ginsu.killcontrol 119 | key 120 | killAfterForcedlLock 121 | label 122 | Lock button press only 123 | dynamicRule 124 | killAfterLockStatus,0 125 | 126 | 127 | cell 128 | PSSwitchCell 129 | cellClass 130 | PSSubtitleSwitchTableCell 131 | cellSubtitleText 132 | Kill apps after a specified time. 133 | default 134 | 135 | defaults 136 | com.ginsu.killcontrol 137 | key 138 | hasGracePeriod 139 | label 140 | Grace period 141 | id 142 | hasGracePeriodStatus 143 | dynamicRule 144 | killAfterLockStatus,0 145 | 146 | 147 | cell 148 | PSSegmentCell 149 | defaults 150 | com.ginsu.killcontrol 151 | default 152 | 1 153 | key 154 | killLockGracePeriodTimeMeasurement 155 | validTitles 156 | 157 | Seconds 158 | Minutes 159 | Hours 160 | 161 | validValues 162 | 163 | 1 164 | 2 165 | 3 166 | 167 | dynamicRule 168 | hasGracePeriodStatus,0 169 | 170 | 171 | cell 172 | PSTableCell 173 | cellClass 174 | GSSliderCell 175 | title 176 | Kill after: 177 | default 178 | 10.0 179 | defaults 180 | com.ginsu.killcontrol 181 | key 182 | killLockGracePeriod 183 | min 184 | 1.0 185 | max 186 | 100.0 187 | dynamicRule 188 | hasGracePeriodStatus,0 189 | 190 | 191 | cell 192 | PSGroupCell 193 | label 194 | Kill/Open rules 195 | footerText 196 | Kill apps of your choice when a specific app is opened. 197 | 198 | 199 | cell 200 | PSSwitchCell 201 | cellClass 202 | PSSubtitleSwitchTableCell 203 | cellSubtitleText 204 | Kill chosen apps when an app opens. 205 | default 206 | 207 | defaults 208 | com.ginsu.killcontrol 209 | key 210 | killOnAppLaunch 211 | label 212 | Kill on app launch 213 | id 214 | killOnAppLaunchStatus 215 | 216 | 217 | bundle 218 | AltList 219 | cell 220 | PSLinkListCell 221 | detail 222 | ATLApplicationListMultiSelectionController 223 | key 224 | killTheseAppsOne 225 | defaults 226 | com.ginsu.killcontrol 227 | sections 228 | 229 | 230 | sectionType 231 | Visible 232 | 233 | 234 | isController 235 | 236 | overridePrincipalClass 237 | 238 | label 239 | Kill these apps 240 | useSearchBar 241 | 242 | alphabeticIndexingEnabled 243 | 244 | dynamicRule 245 | killOnAppLaunchStatus,0 246 | 247 | 248 | bundle 249 | AltList 250 | cell 251 | PSLinkListCell 252 | detail 253 | ATLApplicationListSelectionController 254 | key 255 | whenThisAppOpensOne 256 | defaults 257 | com.ginsu.killcontrol 258 | sections 259 | 260 | 261 | sectionType 262 | Visible 263 | 264 | 265 | isController 266 | 267 | overridePrincipalClass 268 | 269 | label 270 | When this app opens 271 | useSearchBar 272 | 273 | alphabeticIndexingEnabled 274 | 275 | dynamicRule 276 | killOnAppLaunchStatus,0 277 | 278 | 279 | cell 280 | PSGroupCell 281 | footerText 282 | Open apps of your choice in the background when a specific app is opened. 283 | 284 | 285 | cell 286 | PSSwitchCell 287 | cellClass 288 | PSSubtitleSwitchTableCell 289 | cellSubtitleText 290 | Open chosen apps when an app opens. 291 | default 292 | 293 | defaults 294 | com.ginsu.killcontrol 295 | key 296 | openOnAppLaunch 297 | label 298 | Open on app launch 299 | id 300 | openOnAppLaunchStatus 301 | 302 | 303 | bundle 304 | AltList 305 | cell 306 | PSLinkListCell 307 | detail 308 | ATLApplicationListMultiSelectionController 309 | key 310 | openTheseAppsTwo 311 | defaults 312 | com.ginsu.killcontrol 313 | sections 314 | 315 | 316 | sectionType 317 | Visible 318 | 319 | 320 | isController 321 | 322 | overridePrincipalClass 323 | 324 | label 325 | Open these apps 326 | useSearchBar 327 | 328 | alphabeticIndexingEnabled 329 | 330 | dynamicRule 331 | openOnAppLaunchStatus,0 332 | 333 | 334 | bundle 335 | AltList 336 | cell 337 | PSLinkListCell 338 | detail 339 | ATLApplicationListSelectionController 340 | key 341 | whenThisAppOpensTwo 342 | defaults 343 | com.ginsu.killcontrol 344 | sections 345 | 346 | 347 | sectionType 348 | Visible 349 | 350 | 351 | isController 352 | 353 | overridePrincipalClass 354 | 355 | label 356 | When this app opens 357 | useSearchBar 358 | 359 | alphabeticIndexingEnabled 360 | 361 | dynamicRule 362 | openOnAppLaunchStatus,0 363 | 364 | 365 | cell 366 | PSGroupCell 367 | label 368 | White listing 369 | footerText 370 | Prevent selected apps from being killed. While the device is locked, if a black list has been configured under the 'Device Lock' section, the white list and now playing app options will be ignored. 371 | 372 | 373 | bundle 374 | AltList 375 | cell 376 | PSLinkListCell 377 | detail 378 | ATLApplicationListMultiSelectionController 379 | key 380 | whitelistApps 381 | defaults 382 | com.ginsu.killcontrol 383 | sections 384 | 385 | 386 | sectionType 387 | Visible 388 | 389 | 390 | isController 391 | 392 | overridePrincipalClass 393 | 394 | label 395 | White list 396 | useSearchBar 397 | 398 | alphabeticIndexingEnabled 399 | 400 | 401 | 402 | cell 403 | PSSwitchCell 404 | cellClass 405 | PSSubtitleSwitchTableCell 406 | cellSubtitleText 407 | Don't kill music apps 408 | default 409 | 410 | defaults 411 | com.ginsu.killcontrol 412 | key 413 | excludeMediaApps 414 | label 415 | Exclude now playing app 416 | 417 | 418 | cell 419 | PSSwitchCell 420 | cellClass 421 | PSSubtitleSwitchTableCell 422 | cellSubtitleText 423 | Prevent swipe on whitelisted apps 424 | default 425 | 426 | defaults 427 | com.ginsu.killcontrol 428 | key 429 | preventSwipe 430 | label 431 | Prevent swipe up 432 | 433 | 434 | cell 435 | PSSwitchCell 436 | cellClass 437 | PSSubtitleSwitchTableCell 438 | cellSubtitleText 439 | A shortcut to whitelisting apps 440 | default 441 | 442 | defaults 443 | com.ginsu.killcontrol 444 | key 445 | useWhiteListGesture 446 | label 447 | White list gesture 448 | id 449 | useWhiteListGestureStatus 450 | 451 | 452 | cell 453 | PSSegmentCell 454 | defaults 455 | com.ginsu.killcontrol 456 | default 457 | 1 458 | key 459 | whiteListShortcut 460 | validTitles 461 | 462 | One finger hold 463 | Two finger hold 464 | 465 | validValues 466 | 467 | 1 468 | 2 469 | 470 | dynamicRule 471 | useWhiteListGestureStatus,0 472 | 473 | 474 | cell 475 | PSGroupCell 476 | label 477 | Miscellaneous 478 | footerText 479 | Ask before proceeding to kill all apps. 480 | 481 | 482 | cell 483 | PSSwitchCell 484 | cellClass 485 | PSSubtitleSwitchTableCell 486 | cellSubtitleText 487 | Shows an alert before killing all 488 | default 489 | 490 | defaults 491 | com.ginsu.killcontrol 492 | key 493 | askBeforeKilling 494 | label 495 | Ask before killing 496 | 497 | 498 | title 499 | Behaviour 500 | 501 | 502 | -------------------------------------------------------------------------------- /killcontrol/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | killcontrol 9 | CFBundleIdentifier 10 | com.ginsu.killcontrol 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1.0 21 | NSPrincipalClass 22 | killcontrol.RootListController 23 | 24 | 25 | -------------------------------------------------------------------------------- /killcontrol/Resources/PrefIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginsudev/KillControl/0b97e952c9304408a41aec5f7bcaca5464c7daaa/killcontrol/Resources/PrefIcon.png -------------------------------------------------------------------------------- /killcontrol/Resources/PrefIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginsudev/KillControl/0b97e952c9304408a41aec5f7bcaca5464c7daaa/killcontrol/Resources/PrefIcon@2x.png -------------------------------------------------------------------------------- /killcontrol/Resources/PrefIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginsudev/KillControl/0b97e952c9304408a41aec5f7bcaca5464c7daaa/killcontrol/Resources/PrefIcon@3x.png -------------------------------------------------------------------------------- /killcontrol/Resources/Root.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | items 6 | 7 | 8 | cell 9 | PSSwitchCell 10 | cellClass 11 | PSSubtitleSwitchTableCell 12 | cellSubtitleText 13 | Enable/Disable the tweak 14 | default 15 | 16 | defaults 17 | com.ginsu.killcontrol 18 | key 19 | isEnabled 20 | label 21 | Enabled 22 | 23 | 24 | cell 25 | PSGroupCell 26 | label 27 | Configuration 28 | 29 | 30 | cell 31 | PSLinkCell 32 | detail 33 | killcontrol.KillControlBehaviourController 34 | isController 35 | 36 | label 37 | Behaviour 38 | 39 | 40 | cell 41 | PSGroupCell 42 | label 43 | Repo 44 | 45 | 46 | cell 47 | PSButtonCell 48 | cellClass 49 | GSRepoCell 50 | link 51 | https://repo.ginsu.dev 52 | title 53 | Add Ginsu's repo 54 | 55 | 56 | cell 57 | PSGroupCell 58 | footerText 59 | © 2022 Ginsu (@ginsudev) 60 | Icon made by @bank5ia 61 | footerAlignment 62 | 1 63 | 64 | 65 | title 66 | KillControl 67 | 68 | 69 | -------------------------------------------------------------------------------- /killcontrol/Resources/defaults.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | isEnabled 6 | 7 | killAfterLock 8 | 9 | killAfterForcedlLock 10 | 11 | hasGracePeriod 12 | 13 | killLockGracePeriodTimeMeasurement 14 | 1 15 | killLockGracePeriod 16 | 10.0 17 | killOnAppLaunch 18 | 19 | openOnAppLaunch 20 | 21 | swipeDownToKillAll 22 | 23 | askBeforeKilling 24 | 25 | excludeMediaApps 26 | 27 | onlyKillChosenWhenLocked 28 | 29 | preventSwipe 30 | 31 | useWhiteListGesture 32 | 33 | whiteListShortcut 34 | 1 35 | 36 | 37 | -------------------------------------------------------------------------------- /killcontrol/Sources/killcontrol/KillControlBehaviourController.swift: -------------------------------------------------------------------------------- 1 | import Preferences 2 | import Foundation 3 | import killcontrolC 4 | 5 | class KillControlBehaviourController: PSListController { 6 | 7 | override var specifiers: NSMutableArray? { 8 | get { 9 | if let specifiers = value(forKey: "_specifiers") as? NSMutableArray { 10 | self.collectDynamicSpecifiersFromArray(specifiers) 11 | return specifiers 12 | } else { 13 | let specifiers = loadSpecifiers(fromPlistName: "Behaviour", target: self) 14 | setValue(specifiers, forKey: "_specifiers") 15 | self.collectDynamicSpecifiersFromArray(specifiers!) 16 | return specifiers 17 | } 18 | } 19 | set { 20 | super.specifiers = newValue 21 | } 22 | } 23 | 24 | var hasDynamicSpecifiers: Bool! 25 | var dynamicSpecifiers = [String : [PSSpecifier]]() 26 | var hiddenSpecifiers = [PSSpecifier]() 27 | 28 | override func reloadSpecifiers() { 29 | super.reloadSpecifiers() 30 | self.collectDynamicSpecifiersFromArray(self.specifiers!) 31 | } 32 | 33 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 34 | guard hasDynamicSpecifiers else { 35 | return UITableView.automaticDimension 36 | } 37 | 38 | if let dynamicSpecifier = specifier(at: indexPath) { 39 | for array in dynamicSpecifiers.values { 40 | if array.contains(dynamicSpecifier) { 41 | let shouldHide = shouldHideSpecifier(dynamicSpecifier) 42 | 43 | if let specifierCell: UITableViewCell = dynamicSpecifier.property(forKey: PSTableCellKey) as? UITableViewCell { 44 | specifierCell.clipsToBounds = shouldHide 45 | 46 | if shouldHide { 47 | if !hiddenSpecifiers.contains(dynamicSpecifier) { 48 | hiddenSpecifiers.append(dynamicSpecifier) 49 | } 50 | return 0 51 | } 52 | 53 | } 54 | } else { 55 | if hiddenSpecifiers.contains(dynamicSpecifier) { 56 | hiddenSpecifiers = hiddenSpecifiers.filter({$0 != dynamicSpecifier}) 57 | } 58 | } 59 | } 60 | } 61 | 62 | return UITableView.automaticDimension 63 | } 64 | 65 | func shouldHideSpecifier(_ specifier: PSSpecifier) -> Bool { 66 | let dynamicSpecifierRule = specifier.property(forKey: "dynamicRule") as! String 67 | let components: [String] = dynamicSpecifierRule.components(separatedBy: ",") 68 | let opposingSpecifier = self.specifier(forID: components.first) 69 | let opposingValue: NSNumber = self.readPreferenceValue(opposingSpecifier) as! NSNumber 70 | let requiredValueString: String = components.last! 71 | let requiredValue = Int(requiredValueString) 72 | 73 | //Hide for all values except one... Useful for list controllers. 74 | if components.count == 3 { 75 | let shouldStayVisible: Bool = components[1] == "s" //s for show, h for hide. 76 | 77 | if shouldStayVisible { 78 | if hiddenSpecifiers.contains(opposingSpecifier!) { 79 | return true 80 | } 81 | 82 | if opposingValue.intValue == requiredValue { 83 | return false 84 | } 85 | 86 | return true 87 | } 88 | } 89 | 90 | //If there's no h or s in the dynamicRule, don't do anything fancy... 91 | return hiddenSpecifiers.contains(opposingSpecifier!) || opposingValue.intValue == requiredValue 92 | } 93 | 94 | func collectDynamicSpecifiersFromArray(_ array: NSArray) { 95 | if !self.dynamicSpecifiers.isEmpty { 96 | self.dynamicSpecifiers.removeAll() 97 | } 98 | 99 | var dynamicSpecifiersArray: [PSSpecifier] = [PSSpecifier]() 100 | 101 | for item in array { 102 | if let item = item as? PSSpecifier { 103 | if let dynamicSpecifierRule = item.property(forKey: "dynamicRule") as? String { 104 | if dynamicSpecifierRule.count > 0 { 105 | dynamicSpecifiersArray.append(item) 106 | } 107 | } 108 | } 109 | } 110 | 111 | let groupedDict = Dictionary(grouping: dynamicSpecifiersArray, by: {($0.property(forKey: "dynamicRule") as! String).components(separatedBy: ",").first!}) 112 | 113 | for key in groupedDict.keys { 114 | let sortedSpecifiers = groupedDict[key]! 115 | dynamicSpecifiers[key] = sortedSpecifiers 116 | } 117 | 118 | self.hasDynamicSpecifiers = (self.dynamicSpecifiers.count > 0) 119 | } 120 | 121 | override func viewDidLoad() { 122 | super.viewDidLoad() 123 | self.table.keyboardDismissMode = .onDrag 124 | 125 | let applyButton = GSRespringButton() 126 | self.navigationItem.rightBarButtonItem = applyButton 127 | } 128 | 129 | override func readPreferenceValue(_ specifier: PSSpecifier!) -> Any! { 130 | let path = "/User/Library/Preferences/com.ginsu.killcontrol.plist" 131 | let settings = NSMutableDictionary() 132 | let pathDict = NSDictionary(contentsOfFile: path) 133 | settings.addEntries(from: pathDict as! [AnyHashable : Any]) 134 | return ((settings[specifier.properties["key"]!]) != nil) ? settings[specifier.properties["key"]!] : specifier.properties["default"] 135 | } 136 | 137 | override func setPreferenceValue(_ value: Any!, specifier: PSSpecifier!) { 138 | let path = "/User/Library/Preferences/com.ginsu.killcontrol.plist" 139 | let settings = NSMutableDictionary() 140 | let pathDict = NSDictionary(contentsOfFile: path) 141 | settings.addEntries(from: pathDict as! [AnyHashable : Any]) 142 | settings.setObject(value!, forKey: specifier.properties["key"] as! NSCopying) 143 | settings.write(toFile: path, atomically: true) 144 | 145 | if hasDynamicSpecifiers { 146 | if let specifierID = specifier.property(forKey: PSIDKey) as? String { 147 | let dynamicSpecifier = self.dynamicSpecifiers[specifierID] 148 | 149 | guard dynamicSpecifier != nil else { 150 | return 151 | } 152 | 153 | self.table.reloadData() 154 | } 155 | } 156 | } 157 | 158 | override func viewWillAppear(_ animated: Bool) { 159 | super.viewWillAppear(animated) 160 | 161 | self.reloadSpecifiers() 162 | self.table.reloadData() 163 | } 164 | 165 | override func tableViewStyle() -> UITableView.Style { 166 | if #available(iOS 13.0, *) { 167 | return .insetGrouped 168 | } else { 169 | return .grouped 170 | } 171 | } 172 | 173 | override func _returnKeyPressed(_ arg1: Any!) { 174 | self.view.endEditing(true) 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /killcontrol/Sources/killcontrol/RootListController.swift: -------------------------------------------------------------------------------- 1 | import Preferences 2 | import killcontrolC 3 | 4 | class RootListController: PSListController { 5 | override var specifiers: NSMutableArray? { 6 | get { 7 | if let specifiers = value(forKey: "_specifiers") as? NSMutableArray { 8 | return specifiers 9 | } else { 10 | let specifiers = loadSpecifiers(fromPlistName: "Root", target: self) 11 | setValue(specifiers, forKey: "_specifiers") 12 | return specifiers 13 | } 14 | } 15 | set { 16 | super.specifiers = newValue 17 | } 18 | } 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | self.table.keyboardDismissMode = .onDrag 23 | 24 | if let icon = UIImage(named: "/Library/PreferenceBundles/killcontrol.bundle/PrefIcon.png") { 25 | self.navigationItem.titleView = UIImageView(image: icon) 26 | } 27 | 28 | let applyButton = GSRespringButton() 29 | self.navigationItem.rightBarButtonItem = applyButton 30 | } 31 | 32 | override func readPreferenceValue(_ specifier: PSSpecifier!) -> Any! { 33 | let path = "/User/Library/Preferences/com.ginsu.killcontrol.plist" 34 | let settings = NSMutableDictionary() 35 | let pathDict = NSDictionary(contentsOfFile: path) 36 | settings.addEntries(from: pathDict as! [AnyHashable : Any]) 37 | return ((settings[specifier.properties["key"]!]) != nil) ? settings[specifier.properties["key"]!] : specifier.properties["default"] 38 | } 39 | 40 | override func setPreferenceValue(_ value: Any!, specifier: PSSpecifier!) { 41 | let path = "/User/Library/Preferences/com.ginsu.killcontrol.plist" 42 | let settings = NSMutableDictionary() 43 | let pathDict = NSDictionary(contentsOfFile: path) 44 | settings.addEntries(from: pathDict as! [AnyHashable : Any]) 45 | settings.setObject(value!, forKey: specifier.properties["key"] as! NSCopying) 46 | settings.write(toFile: path, atomically: true) 47 | } 48 | 49 | override func tableViewStyle() -> UITableView.Style { 50 | if #available(iOS 13.0, *) { 51 | return .insetGrouped 52 | } else { 53 | return .grouped 54 | } 55 | } 56 | 57 | override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 58 | if (section == 0) { 59 | return GSHeaderView(frame: CGRect(x: 0, y: 0, width: tableView.frame.size.width, height: 200), 60 | twitterHandle: "ginsudev", 61 | developerName: "Ginsu", 62 | tweakName: "KillControl", 63 | tweakVersion: "v1.2.2", 64 | email: "njl02@outlook.com", 65 | discordURL: "https://discord.gg/BhdUyCbgkZ", 66 | donateURL: "https://paypal.me/xiaonuoya") 67 | } 68 | 69 | return nil 70 | } 71 | 72 | override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 73 | return section == 0 ? 150 : 45 74 | } 75 | 76 | override func _returnKeyPressed(_ arg1: Any!) { 77 | self.view.endEditing(true) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /killcontrol/Sources/killcontrolC/include/killcontrol.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface UIView (Private) 6 | - (UIViewController *)_viewControllerForAncestor; 7 | @end 8 | 9 | @interface PSListController (Private) 10 | - (void)_returnKeyPressed:(id)arg1; 11 | @end 12 | 13 | @interface PSSpecifier (Private) 14 | -(void)performSetterWithValue:(id)value; 15 | -(id)performGetter; 16 | @end 17 | -------------------------------------------------------------------------------- /killcontrol/Sources/killcontrolC/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module killcontrolC { 2 | umbrella "." 3 | export * 4 | } 5 | -------------------------------------------------------------------------------- /killcontrol/Sources/killcontrolC/killcontrol.m: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /killcontrol/layout/Library/PreferenceLoader/Preferences/killcontrol.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | entry 6 | 7 | bundle 8 | killcontrol 9 | cell 10 | PSLinkCell 11 | detail 12 | killcontrol.RootListController 13 | icon 14 | PrefIcon.png 15 | isController 16 | 17 | label 18 | KillControl 19 | 20 | 21 | 22 | --------------------------------------------------------------------------------