2019-05-20 21:26:01 +02:00
//
// M y A p p s V i e w C o n t r o l l e r . s w i f t
// A l t S t o r e
//
2019-07-19 16:42:40 -07:00
// C r e a t e d b y R i l e y T e s t u t o n 7 / 1 6 / 1 9 .
2019-05-20 21:26:01 +02:00
// C o p y r i g h t © 2 0 1 9 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
2021-09-15 14:27:16 -07:00
import Combine
2023-03-01 00:48:36 -05:00
import Intents
import MobileCoreServices
import UIKit
2019-07-19 16:42:40 -07:00
2019-07-28 15:51:36 -07:00
import AltSign
2023-03-01 00:48:36 -05:00
import SideStoreCore
2023-03-01 14:36:52 -05:00
import RoxasUIKit
2023-04-02 02:28:12 -04:00
import OSLog
#if canImport ( Logging )
import Logging
#endif
2019-07-28 15:51:36 -07:00
2019-08-20 19:06:03 -05:00
import Nuke
2019-07-19 16:42:40 -07:00
private let maximumCollapsedUpdatesCount = 2
2019-06-10 15:03:47 -07:00
2023-03-01 00:48:36 -05:00
extension MyAppsViewController {
private enum Section : Int , CaseIterable {
2019-07-24 15:01:33 -07:00
case noUpdates
2019-07-19 16:42:40 -07:00
case updates
2020-03-11 14:43:19 -07:00
case activeApps
case inactiveApps
2019-07-19 16:42:40 -07:00
}
}
2023-03-01 00:48:36 -05:00
final class MyAppsViewController : UICollectionViewController {
2020-05-16 16:34:50 -07:00
private let coordinator = NSFileCoordinator ( )
2020-05-17 23:44:36 -07:00
private let operationQueue = OperationQueue ( )
2023-03-01 00:48:36 -05:00
2019-05-20 21:26:01 +02:00
private lazy var dataSource = self . makeDataSource ( )
2019-07-24 15:01:33 -07:00
private lazy var noUpdatesDataSource = self . makeNoUpdatesDataSource ( )
2019-07-19 16:42:40 -07:00
private lazy var updatesDataSource = self . makeUpdatesDataSource ( )
2020-03-11 14:43:19 -07:00
private lazy var activeAppsDataSource = self . makeActiveAppsDataSource ( )
private lazy var inactiveAppsDataSource = self . makeInactiveAppsDataSource ( )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
private var prototypeUpdateCell : UpdateCollectionViewCell !
2019-07-28 15:51:36 -07:00
private var sideloadingProgressView : UIProgressView !
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
// S t a t e
private var isUpdateSectionCollapsed = true
private var expandedAppUpdates = Set < String > ( )
private var isRefreshingAllApps = false
2020-03-06 17:08:35 -08:00
private var refreshGroup : RefreshGroup ?
2019-07-28 15:51:36 -07:00
private var sideloadingProgress : Progress ?
2020-03-20 16:32:31 -07:00
private var dropDestinationIndexPath : IndexPath ?
2023-03-01 00:48:36 -05:00
2020-10-01 14:09:45 -07:00
private var _imagePickerInstalledApp : InstalledApp ?
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
// C a c h e
private var cachedUpdateSizes = [ String : CGSize ] ( )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
private lazy var dateFormatter : DateFormatter = {
let dateFormatter = DateFormatter ( )
dateFormatter . dateStyle = . medium
dateFormatter . timeStyle = . none
return dateFormatter
} ( )
2023-03-01 00:48:36 -05:00
required init ? ( coder aDecoder : NSCoder ) {
2019-07-19 16:42:40 -07:00
super . init ( coder : aDecoder )
2023-03-01 00:48:36 -05:00
2019-07-30 17:00:04 -07:00
NotificationCenter . default . addObserver ( self , selector : #selector ( MyAppsViewController . didFetchSource ( _ : ) ) , name : AppManager . didFetchSourceNotification , object : nil )
2023-03-01 19:09:33 -05:00
NotificationCenter . default . addObserver ( self , selector : #selector ( MyAppsViewController . importApp ( _ : ) ) , name : SideStoreAppDelegate . importAppDeepLinkNotification , object : nil )
2019-05-20 21:26:01 +02:00
}
2023-03-01 00:48:36 -05:00
override func viewDidLoad ( ) {
2019-07-19 16:42:40 -07:00
super . viewDidLoad ( )
2023-03-01 00:48:36 -05:00
2019-07-24 15:01:33 -07:00
// A l l o w s u s t o i n t e r c e p t d e l e g a t e c a l l b a c k s .
2023-03-01 00:48:36 -05:00
updatesDataSource . fetchedResultsController . delegate = self
collectionView . dataSource = dataSource
collectionView . prefetchDataSource = dataSource
collectionView . dragDelegate = self
collectionView . dropDelegate = self
collectionView . dragInteractionEnabled = true
prototypeUpdateCell = UpdateCollectionViewCell . instantiate ( with : UpdateCollectionViewCell . nib ! )
prototypeUpdateCell . contentView . translatesAutoresizingMaskIntoConstraints = false
collectionView . register ( UpdateCollectionViewCell . nib , forCellWithReuseIdentifier : " UpdateCell " )
collectionView . register ( UpdatesCollectionHeaderView . self , forSupplementaryViewOfKind : UICollectionView . elementKindSectionHeader , withReuseIdentifier : " UpdatesHeader " )
collectionView . register ( InstalledAppsCollectionHeaderView . self , forSupplementaryViewOfKind : UICollectionView . elementKindSectionHeader , withReuseIdentifier : " ActiveAppsHeader " )
collectionView . register ( InstalledAppsCollectionHeaderView . self , forSupplementaryViewOfKind : UICollectionView . elementKindSectionHeader , withReuseIdentifier : " InactiveAppsHeader " )
sideloadingProgressView = UIProgressView ( progressViewStyle : . bar )
sideloadingProgressView . translatesAutoresizingMaskIntoConstraints = false
sideloadingProgressView . progressTintColor = . altPrimary
sideloadingProgressView . progress = 0
if let navigationBar = navigationController ? . navigationBar {
navigationBar . addSubview ( sideloadingProgressView )
NSLayoutConstraint . activate ( [ sideloadingProgressView . leadingAnchor . constraint ( equalTo : navigationBar . leadingAnchor ) ,
sideloadingProgressView . trailingAnchor . constraint ( equalTo : navigationBar . trailingAnchor ) ,
sideloadingProgressView . bottomAnchor . constraint ( equalTo : navigationBar . bottomAnchor ) ] )
}
if #available ( iOS 13 , * ) { } else {
registerForPreviewing ( with : self , sourceView : collectionView )
}
}
override func viewWillAppear ( _ animated : Bool ) {
2019-08-28 11:13:22 -07:00
super . viewWillAppear ( animated )
2023-03-01 00:48:36 -05:00
updateDataSource ( )
fetchAppIDs ( )
2019-08-28 11:13:22 -07:00
}
2023-03-01 00:48:36 -05:00
override func prepare ( for segue : UIStoryboardSegue , sender : Any ? ) {
2019-09-12 13:51:03 -07:00
guard let identifier = segue . identifier else { return }
2023-03-01 00:48:36 -05:00
switch identifier {
2019-09-12 13:51:03 -07:00
case " showApp " , " showUpdate " :
2023-03-01 00:48:36 -05:00
guard let cell = sender as ? UICollectionViewCell , let indexPath = collectionView . indexPath ( for : cell ) else { return }
let installedApp = dataSource . item ( at : indexPath )
2019-09-12 13:51:03 -07:00
let appViewController = segue . destination as ! AppViewController
appViewController . app = installedApp . storeApp
2023-03-01 00:48:36 -05:00
2019-09-12 13:51:03 -07:00
default : break
}
2019-07-24 12:23:54 -07:00
}
2023-03-01 00:48:36 -05:00
override func shouldPerformSegue ( withIdentifier identifier : String , sender : Any ? ) -> Bool {
2019-07-28 15:51:36 -07:00
guard identifier = = " showApp " else { return true }
2023-03-01 00:48:36 -05:00
guard let cell = sender as ? UICollectionViewCell , let indexPath = collectionView . indexPath ( for : cell ) else { return true }
let installedApp = dataSource . item ( at : indexPath )
2019-07-28 15:51:36 -07:00
return ! installedApp . isSideloaded
}
2023-03-01 00:48:36 -05:00
@IBAction func unwindToMyAppsViewController ( _ : UIStoryboardSegue ) { }
2019-05-20 21:26:01 +02:00
}
2023-03-01 00:48:36 -05:00
private extension MyAppsViewController {
func makeDataSource ( ) -> RSTCompositeCollectionViewPrefetchingDataSource < InstalledApp , UIImage > {
let dataSource = RSTCompositeCollectionViewPrefetchingDataSource < InstalledApp , UIImage > ( dataSources : [ noUpdatesDataSource , updatesDataSource , activeAppsDataSource , inactiveAppsDataSource ] )
2019-07-19 16:42:40 -07:00
dataSource . proxy = self
return dataSource
}
2023-03-01 00:48:36 -05:00
func makeNoUpdatesDataSource ( ) -> RSTDynamicCollectionViewPrefetchingDataSource < InstalledApp , UIImage > {
2019-07-24 15:01:33 -07:00
let dynamicDataSource = RSTDynamicCollectionViewPrefetchingDataSource < InstalledApp , UIImage > ( )
dynamicDataSource . numberOfSectionsHandler = { 1 }
dynamicDataSource . numberOfItemsHandler = { _ in self . updatesDataSource . itemCount = = 0 ? 1 : 0 }
dynamicDataSource . cellIdentifierHandler = { _ in " NoUpdatesCell " }
2023-03-01 00:48:36 -05:00
dynamicDataSource . cellConfigurationHandler = { cell , _ , _ in
2019-10-23 13:32:17 -07:00
let cell = cell as ! NoUpdatesCollectionViewCell
cell . layoutMargins . left = self . view . layoutMargins . left
cell . layoutMargins . right = self . view . layoutMargins . right
2023-03-01 00:48:36 -05:00
2019-10-23 13:32:17 -07:00
cell . blurView . layer . cornerRadius = 20
cell . blurView . layer . masksToBounds = true
cell . blurView . backgroundColor = . altPrimary
2019-07-24 15:01:33 -07:00
}
2023-03-01 00:48:36 -05:00
2019-07-24 15:01:33 -07:00
return dynamicDataSource
}
2023-03-01 00:48:36 -05:00
func makeUpdatesDataSource ( ) -> RSTFetchedResultsCollectionViewPrefetchingDataSource < InstalledApp , UIImage > {
2019-07-24 13:52:58 -07:00
let fetchRequest = InstalledApp . updatesFetchRequest ( )
2022-09-12 17:05:55 -07:00
fetchRequest . sortDescriptors = [ NSSortDescriptor ( keyPath : \ InstalledApp . storeApp ? . latestVersion ? . date , ascending : true ) ,
2019-07-28 15:08:13 -07:00
NSSortDescriptor ( keyPath : \ InstalledApp . name , ascending : true ) ]
2019-05-20 21:26:01 +02:00
fetchRequest . returnsObjectsAsFaults = false
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource < InstalledApp , UIImage > ( fetchRequest : fetchRequest , managedObjectContext : DatabaseManager . shared . viewContext )
dataSource . liveFetchLimit = maximumCollapsedUpdatesCount
dataSource . cellIdentifierHandler = { _ in " UpdateCell " }
2023-03-01 00:48:36 -05:00
dataSource . cellConfigurationHandler = { [ weak self ] cell , installedApp , _ in
2019-09-12 13:51:03 -07:00
guard let self = self else { return }
2022-09-12 17:05:55 -07:00
guard let app = installedApp . storeApp , let latestVersion = app . latestVersion else { return }
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
let cell = cell as ! UpdateCollectionViewCell
2019-10-23 13:32:17 -07:00
cell . layoutMargins . left = self . view . layoutMargins . left
cell . layoutMargins . right = self . view . layoutMargins . right
2023-03-01 00:48:36 -05:00
2019-09-19 11:29:10 -07:00
cell . tintColor = app . tintColor ? ? . altPrimary
2019-07-19 16:42:40 -07:00
cell . versionDescriptionTextView . text = app . versionDescription
2023-03-01 00:48:36 -05:00
2019-10-23 13:32:17 -07:00
cell . bannerView . iconImageView . image = nil
cell . bannerView . iconImageView . isIndicatingActivity = true
2023-03-01 00:48:36 -05:00
2020-08-27 15:23:21 -07:00
cell . bannerView . configure ( for : app )
2023-03-01 00:48:36 -05:00
2022-09-12 17:05:55 -07:00
let versionDate = Date ( ) . relativeDateString ( since : latestVersion . date , dateFormatter : self . dateFormatter )
2020-08-27 15:23:21 -07:00
cell . bannerView . subtitleLabel . text = versionDate
2023-03-01 00:48:36 -05:00
2020-08-27 15:23:21 -07:00
let appName : String
2023-03-01 00:48:36 -05:00
if app . isBeta {
2020-08-27 15:23:21 -07:00
appName = String ( format : NSLocalizedString ( " %@ beta " , comment : " " ) , app . name )
2023-03-01 00:48:36 -05:00
} else {
2020-08-27 15:23:21 -07:00
appName = app . name
}
2023-03-01 00:48:36 -05:00
2022-09-12 17:05:55 -07:00
cell . bannerView . accessibilityLabel = String ( format : NSLocalizedString ( " %@ %@ update. Released on %@. " , comment : " " ) , appName , latestVersion . version , versionDate )
2023-03-01 00:48:36 -05:00
2019-10-23 13:32:17 -07:00
cell . bannerView . button . isIndicatingActivity = false
cell . bannerView . button . addTarget ( self , action : #selector ( MyAppsViewController . updateApp ( _ : ) ) , for : . primaryActionTriggered )
2020-08-27 15:23:21 -07:00
cell . bannerView . button . accessibilityLabel = String ( format : NSLocalizedString ( " Update %@ " , comment : " " ) , installedApp . name )
2023-03-01 00:48:36 -05:00
if self . expandedAppUpdates . contains ( app . bundleIdentifier ) {
2019-07-19 16:42:40 -07:00
cell . mode = . expanded
2023-03-01 00:48:36 -05:00
} else {
2019-07-19 16:42:40 -07:00
cell . mode = . collapsed
2019-06-06 12:56:13 -07:00
}
2023-03-01 00:48:36 -05:00
2019-07-24 12:23:54 -07:00
cell . versionDescriptionTextView . moreButton . addTarget ( self , action : #selector ( MyAppsViewController . toggleUpdateCellMode ( _ : ) ) , for : . primaryActionTriggered )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
let progress = AppManager . shared . installationProgress ( for : app )
2019-10-23 13:32:17 -07:00
cell . bannerView . button . progress = progress
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
cell . setNeedsLayout ( )
2019-05-20 21:26:01 +02:00
}
2023-03-01 00:48:36 -05:00
dataSource . prefetchHandler = { installedApp , _ , completionHandler in
2019-08-20 19:06:03 -05:00
guard let iconURL = installedApp . storeApp ? . iconURL else { return nil }
2023-03-01 00:48:36 -05:00
return RSTAsyncBlockOperation { operation in
ImagePipeline . shared . loadImage ( with : iconURL , progress : nil , completion : { response , error in
2019-08-27 16:00:59 -07:00
guard ! operation . isCancelled else { return operation . finish ( ) }
2023-03-01 00:48:36 -05:00
if let image = response ? . image {
2019-08-20 19:06:03 -05:00
completionHandler ( image , nil )
2023-03-01 00:48:36 -05:00
} else {
2019-08-20 19:06:03 -05:00
completionHandler ( nil , error )
}
} )
}
}
2023-03-01 00:48:36 -05:00
dataSource . prefetchCompletionHandler = { cell , image , _ , error in
2019-08-20 19:06:03 -05:00
let cell = cell as ! UpdateCollectionViewCell
2019-10-23 13:32:17 -07:00
cell . bannerView . iconImageView . isIndicatingActivity = false
cell . bannerView . iconImageView . image = image
2023-03-01 00:48:36 -05:00
if let error = error {
2023-03-02 00:40:11 -05:00
os_log ( " Error loading image: %@ " , type : . error , error . localizedDescription )
2019-08-20 19:06:03 -05:00
}
}
2023-03-01 00:48:36 -05:00
2019-05-20 21:26:01 +02:00
return dataSource
}
2023-03-01 00:48:36 -05:00
func makeActiveAppsDataSource ( ) -> RSTFetchedResultsCollectionViewPrefetchingDataSource < InstalledApp , UIImage > {
2020-03-11 14:43:19 -07:00
let fetchRequest = InstalledApp . activeAppsFetchRequest ( )
2019-07-28 15:08:13 -07:00
fetchRequest . relationshipKeyPathsForPrefetching = [ # keyPath ( InstalledApp . storeApp ) ]
2019-07-19 16:42:40 -07:00
fetchRequest . sortDescriptors = [ NSSortDescriptor ( keyPath : \ InstalledApp . expirationDate , ascending : true ) ,
NSSortDescriptor ( keyPath : \ InstalledApp . refreshedDate , ascending : false ) ,
2019-07-28 15:08:13 -07:00
NSSortDescriptor ( keyPath : \ InstalledApp . name , ascending : true ) ]
2019-07-19 16:42:40 -07:00
fetchRequest . returnsObjectsAsFaults = false
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource < InstalledApp , UIImage > ( fetchRequest : fetchRequest , managedObjectContext : DatabaseManager . shared . viewContext )
dataSource . cellIdentifierHandler = { _ in " AppCell " }
2023-03-01 00:48:36 -05:00
dataSource . cellConfigurationHandler = { cell , installedApp , indexPath in
2019-09-19 11:29:10 -07:00
let tintColor = installedApp . storeApp ? . tintColor ? ? . altPrimary
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
let cell = cell as ! InstalledAppCollectionViewCell
2019-10-23 13:32:17 -07:00
cell . layoutMargins . left = self . view . layoutMargins . left
cell . layoutMargins . right = self . view . layoutMargins . right
2019-07-19 16:42:40 -07:00
cell . tintColor = tintColor
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
cell . deactivateBadge ? . isHidden = false
2023-03-01 00:48:36 -05:00
if let dropIndexPath = self . dropDestinationIndexPath , dropIndexPath . section = = Section . activeApps . rawValue && dropIndexPath . item = = indexPath . item {
2020-03-20 16:32:31 -07:00
cell . bannerView . alpha = 0.4
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
cell . deactivateBadge ? . alpha = 1.0
cell . deactivateBadge ? . transform = . identity
2023-03-01 00:48:36 -05:00
} else {
2020-03-20 16:32:31 -07:00
cell . bannerView . alpha = 1.0
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
cell . deactivateBadge ? . alpha = 0.0
cell . deactivateBadge ? . transform = CGAffineTransform . identity . scaledBy ( x : 0.33 , y : 0.33 )
}
2023-03-01 00:48:36 -05:00
2020-08-27 15:23:21 -07:00
cell . bannerView . configure ( for : installedApp )
2023-03-01 00:48:36 -05:00
2019-10-23 13:32:17 -07:00
cell . bannerView . iconImageView . isIndicatingActivity = true
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
cell . bannerView . buttonLabel . isHidden = false
cell . bannerView . buttonLabel . text = NSLocalizedString ( " Expires in " , comment : " " )
2023-03-01 00:48:36 -05:00
2019-10-23 13:32:17 -07:00
cell . bannerView . button . isIndicatingActivity = false
2020-03-20 15:52:11 -07:00
cell . bannerView . button . removeTarget ( self , action : nil , for : . primaryActionTriggered )
2019-10-23 13:32:17 -07:00
cell . bannerView . button . addTarget ( self , action : #selector ( MyAppsViewController . refreshApp ( _ : ) ) , for : . primaryActionTriggered )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
let currentDate = Date ( )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
let numberOfDays = installedApp . expirationDate . numberOfCalendarDays ( since : currentDate )
2020-08-27 15:23:21 -07:00
let numberOfDaysText : String
2023-03-01 00:48:36 -05:00
if numberOfDays = = 1 {
2020-08-27 15:23:21 -07:00
numberOfDaysText = NSLocalizedString ( " 1 day " , comment : " " )
2023-03-01 00:48:36 -05:00
} else {
2020-08-27 15:23:21 -07:00
numberOfDaysText = String ( format : NSLocalizedString ( " %@ days " , comment : " " ) , NSNumber ( value : numberOfDays ) )
2019-07-19 16:42:40 -07:00
}
2023-03-01 00:48:36 -05:00
2020-08-27 15:23:21 -07:00
cell . bannerView . button . setTitle ( numberOfDaysText . uppercased ( ) , for : . normal )
cell . bannerView . button . accessibilityLabel = String ( format : NSLocalizedString ( " Refresh %@ " , comment : " " ) , installedApp . name )
2023-03-01 00:48:36 -05:00
2020-08-27 15:23:21 -07:00
cell . bannerView . accessibilityLabel ? += " . " + String ( format : NSLocalizedString ( " Expires in %@ " , comment : " " ) , numberOfDaysText )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
// M a k e s u r e r e f r e s h b u t t o n i s c o r r e c t s i z e .
cell . layoutIfNeeded ( )
2023-03-01 00:48:36 -05:00
switch numberOfDays {
case 2 . . . 3 : cell . bannerView . button . tintColor = . refreshOrange
case 4 . . . 5 : cell . bannerView . button . tintColor = . refreshYellow
2019-10-23 13:32:17 -07:00
case 6. . . : cell . bannerView . button . tintColor = . refreshGreen
default : cell . bannerView . button . tintColor = . refreshRed
2019-07-19 16:42:40 -07:00
}
2023-03-01 00:48:36 -05:00
if let progress = AppManager . shared . refreshProgress ( for : installedApp ) , progress . fractionCompleted < 1.0 {
2019-10-23 13:32:17 -07:00
cell . bannerView . button . progress = progress
2023-03-01 00:48:36 -05:00
} else {
2019-10-23 13:32:17 -07:00
cell . bannerView . button . progress = nil
2019-07-19 16:42:40 -07:00
}
}
2023-03-01 00:48:36 -05:00
dataSource . prefetchHandler = { item , _ , completion in
RSTAsyncBlockOperation { _ in
2020-10-01 14:09:45 -07:00
item . managedObjectContext ? . perform {
2023-03-01 00:48:36 -05:00
item . loadIcon { result in
switch result {
case let . failure ( error ) : completion ( nil , error )
case let . success ( image ) : completion ( image , nil )
2020-10-01 14:09:45 -07:00
}
}
2019-07-28 15:51:36 -07:00
}
}
}
2023-03-01 00:48:36 -05:00
dataSource . prefetchCompletionHandler = { cell , image , _ , _ in
2019-07-28 15:51:36 -07:00
let cell = cell as ! InstalledAppCollectionViewCell
2019-10-23 13:32:17 -07:00
cell . bannerView . iconImageView . image = image
cell . bannerView . iconImageView . isIndicatingActivity = false
2019-07-28 15:51:36 -07:00
}
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
return dataSource
2019-06-06 12:56:13 -07:00
}
2023-03-01 00:48:36 -05:00
func makeInactiveAppsDataSource ( ) -> RSTFetchedResultsCollectionViewPrefetchingDataSource < InstalledApp , UIImage > {
2020-03-11 14:43:19 -07:00
let fetchRequest = InstalledApp . fetchRequest ( ) as NSFetchRequest < InstalledApp >
fetchRequest . relationshipKeyPathsForPrefetching = [ # keyPath ( InstalledApp . storeApp ) ]
fetchRequest . predicate = NSPredicate ( format : " %K == NO " , # keyPath ( InstalledApp . isActive ) )
fetchRequest . sortDescriptors = [ NSSortDescriptor ( keyPath : \ InstalledApp . expirationDate , ascending : true ) ,
NSSortDescriptor ( keyPath : \ InstalledApp . refreshedDate , ascending : false ) ,
NSSortDescriptor ( keyPath : \ InstalledApp . name , ascending : true ) ]
fetchRequest . returnsObjectsAsFaults = false
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource < InstalledApp , UIImage > ( fetchRequest : fetchRequest , managedObjectContext : DatabaseManager . shared . viewContext )
dataSource . cellIdentifierHandler = { _ in " AppCell " }
2023-03-01 00:48:36 -05:00
dataSource . cellConfigurationHandler = { cell , installedApp , _ in
2020-03-11 14:43:19 -07:00
let tintColor = installedApp . storeApp ? . tintColor ? ? . altPrimary
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
let cell = cell as ! InstalledAppCollectionViewCell
cell . layoutMargins . left = self . view . layoutMargins . left
cell . layoutMargins . right = self . view . layoutMargins . right
cell . tintColor = UIColor . gray
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
cell . bannerView . iconImageView . isIndicatingActivity = true
cell . bannerView . buttonLabel . isHidden = true
2020-03-20 16:32:31 -07:00
cell . bannerView . alpha = 1.0
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
cell . deactivateBadge ? . isHidden = true
cell . deactivateBadge ? . alpha = 0.0
cell . deactivateBadge ? . transform = CGAffineTransform . identity . scaledBy ( x : 0.5 , y : 0.5 )
2023-03-01 00:48:36 -05:00
2020-08-27 15:23:21 -07:00
cell . bannerView . configure ( for : installedApp )
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
cell . bannerView . button . isIndicatingActivity = false
cell . bannerView . button . tintColor = tintColor
cell . bannerView . button . setTitle ( NSLocalizedString ( " ACTIVATE " , comment : " " ) , for : . normal )
2020-03-20 15:52:11 -07:00
cell . bannerView . button . removeTarget ( self , action : nil , for : . primaryActionTriggered )
2020-03-11 14:43:19 -07:00
cell . bannerView . button . addTarget ( self , action : #selector ( MyAppsViewController . activateApp ( _ : ) ) , for : . primaryActionTriggered )
2020-08-27 15:23:21 -07:00
cell . bannerView . button . accessibilityLabel = String ( format : NSLocalizedString ( " Activate %@ " , comment : " " ) , installedApp . name )
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
// M a k e s u r e r e f r e s h b u t t o n i s c o r r e c t s i z e .
cell . layoutIfNeeded ( )
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
// E n s u r e n o l e f t o v e r p r o g r e s s f r o m a c t i v e a p p s c e l l r e u s e .
cell . bannerView . button . progress = nil
2023-03-01 00:48:36 -05:00
if let progress = AppManager . shared . refreshProgress ( for : installedApp ) , progress . fractionCompleted < 1.0 {
2020-05-16 16:17:18 -07:00
cell . bannerView . button . progress = progress
2023-03-01 00:48:36 -05:00
} else {
2020-05-16 16:17:18 -07:00
cell . bannerView . button . progress = nil
}
2020-03-11 14:43:19 -07:00
}
2023-03-01 00:48:36 -05:00
dataSource . prefetchHandler = { item , _ , completion in
RSTAsyncBlockOperation { _ in
2020-10-01 14:09:45 -07:00
item . managedObjectContext ? . perform {
2023-03-01 00:48:36 -05:00
item . loadIcon { result in
switch result {
case let . failure ( error ) : completion ( nil , error )
case let . success ( image ) : completion ( image , nil )
2020-10-01 14:09:45 -07:00
}
}
2020-03-11 14:43:19 -07:00
}
}
}
2023-03-01 00:48:36 -05:00
dataSource . prefetchCompletionHandler = { cell , image , _ , _ in
2020-03-11 14:43:19 -07:00
let cell = cell as ! InstalledAppCollectionViewCell
cell . bannerView . iconImageView . image = image
cell . bannerView . iconImageView . isIndicatingActivity = false
}
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
return dataSource
}
2023-03-01 00:48:36 -05:00
func updateDataSource ( ) {
dataSource . predicate = nil
2019-08-28 11:13:22 -07:00
}
2019-06-06 12:56:13 -07:00
}
2023-03-01 00:48:36 -05:00
private extension MyAppsViewController {
func update ( ) {
if updatesDataSource . itemCount > 0 {
navigationController ? . tabBarItem . badgeValue = String ( describing : updatesDataSource . itemCount )
UIApplication . shared . applicationIconBadgeNumber = Int ( updatesDataSource . itemCount )
} else {
navigationController ? . tabBarItem . badgeValue = nil
2019-07-24 13:52:58 -07:00
UIApplication . shared . applicationIconBadgeNumber = 0
2019-06-21 11:20:03 -07:00
}
2023-03-01 00:48:36 -05:00
if isViewLoaded {
2019-09-12 12:49:19 -07:00
UIView . performWithoutAnimation {
self . collectionView . reloadSections ( IndexSet ( integer : Section . updates . rawValue ) )
}
2023-03-01 00:48:36 -05:00
}
2019-06-21 11:20:03 -07:00
}
2023-03-01 00:48:36 -05:00
func fetchAppIDs ( ) {
AppManager . shared . fetchAppIDs { result in
do {
2020-02-10 17:30:11 -08:00
let ( _ , context ) = try result . get ( )
try context . save ( )
2023-03-01 00:48:36 -05:00
} catch {
2023-03-02 00:40:11 -05:00
os_log ( " Failed to fetch App IDs. %@ " , type : . error , error . localizedDescription )
2020-02-10 17:30:11 -08:00
}
}
}
2023-03-01 00:48:36 -05:00
func refresh ( _ installedApps : [ InstalledApp ] , completionHandler : @ escaping ( [ String : Result < InstalledApp , Error > ] ) -> Void ) {
let group = AppManager . shared . refresh ( installedApps , presentingViewController : self , group : refreshGroup )
group . completionHandler = { results in
2020-03-06 17:08:35 -08:00
DispatchQueue . main . async {
2023-03-01 00:48:36 -05:00
let failures = results . compactMapValues { result -> Error ? in
switch result {
2020-03-06 17:08:35 -08:00
case . failure ( OperationError . cancelled ) : return nil
2023-03-01 00:48:36 -05:00
case let . failure ( error ) : return error
2020-03-06 17:08:35 -08:00
case . success : return nil
}
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
guard ! failures . isEmpty else { return }
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
let toastView : ToastView
2023-03-01 00:48:36 -05:00
if let failure = failures . first , results . count = = 1 {
2020-03-06 17:08:35 -08:00
toastView = ToastView ( error : failure . value )
2023-03-01 00:48:36 -05:00
} else {
2020-03-06 17:08:35 -08:00
let localizedText : String
2023-03-01 00:48:36 -05:00
if failures . count = = 1 {
2020-03-06 17:08:35 -08:00
localizedText = NSLocalizedString ( " Failed to refresh 1 app. " , comment : " " )
2023-03-01 00:48:36 -05:00
} else {
2020-03-06 17:08:35 -08:00
localizedText = String ( format : NSLocalizedString ( " Failed to refresh %@ apps. " , comment : " " ) , NSNumber ( value : failures . count ) )
2019-06-06 12:56:13 -07:00
}
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
let error = failures . first ? . value as NSError ?
2020-03-19 15:02:35 -07:00
let detailText = error ? . localizedFailure ? ? error ? . localizedFailureReason ? ? error ? . localizedDescription
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
toastView = ToastView ( text : localizedText , detailText : detailText )
2020-03-11 14:43:19 -07:00
toastView . preferredDuration = 4.0
2019-06-06 12:56:13 -07:00
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
toastView . show ( in : self )
2019-06-10 15:03:47 -07:00
}
2023-03-01 00:48:36 -05:00
2020-03-06 17:08:35 -08:00
self . refreshGroup = nil
completionHandler ( results )
2019-06-10 15:03:47 -07:00
}
2023-03-01 00:48:36 -05:00
refreshGroup = group
2020-03-06 17:08:35 -08:00
UIView . performWithoutAnimation {
2020-03-11 14:43:19 -07:00
self . collectionView . reloadSections ( [ Section . activeApps . rawValue , Section . inactiveApps . rawValue ] )
2019-06-25 14:35:00 -07:00
}
2019-06-06 12:56:13 -07:00
}
2019-05-20 21:26:01 +02:00
}
2019-05-20 21:36:39 +02:00
2023-03-01 00:48:36 -05:00
private extension MyAppsViewController {
@IBAction func toggleAppUpdates ( _ sender : UIButton ) {
let visibleCells = collectionView . visibleCells
collectionView . performBatchUpdates ( {
2019-07-19 16:42:40 -07:00
self . isUpdateSectionCollapsed . toggle ( )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
UIView . animate ( withDuration : 0.3 , animations : {
2023-03-01 00:48:36 -05:00
if self . isUpdateSectionCollapsed {
2019-07-19 16:42:40 -07:00
self . updatesDataSource . liveFetchLimit = maximumCollapsedUpdatesCount
self . expandedAppUpdates . removeAll ( )
2023-03-01 00:48:36 -05:00
for case let cell as UpdateCollectionViewCell in visibleCells {
2019-07-19 16:42:40 -07:00
cell . mode = . collapsed
}
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
self . cachedUpdateSizes . removeAll ( )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
sender . titleLabel ? . transform = . identity
2023-03-01 00:48:36 -05:00
} else {
2019-07-19 16:42:40 -07:00
self . updatesDataSource . liveFetchLimit = 0
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
sender . titleLabel ? . transform = CGAffineTransform . identity . rotated ( by : . pi )
2019-06-05 11:03:49 -07:00
}
2019-07-19 16:42:40 -07:00
} )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
self . collectionView . collectionViewLayout . invalidateLayout ( )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
} , completion : nil )
}
2023-03-01 00:48:36 -05:00
@IBAction func toggleUpdateCellMode ( _ sender : UIButton ) {
let point = collectionView . convert ( sender . center , from : sender . superview )
guard let indexPath = collectionView . indexPathForItem ( at : point ) else { return }
let installedApp = dataSource . item ( at : indexPath )
let cell = collectionView . cellForItem ( at : indexPath ) as ? UpdateCollectionViewCell
if expandedAppUpdates . contains ( installedApp . bundleIdentifier ) {
expandedAppUpdates . remove ( installedApp . bundleIdentifier )
2019-07-19 16:42:40 -07:00
cell ? . mode = . collapsed
2023-03-01 00:48:36 -05:00
} else {
expandedAppUpdates . insert ( installedApp . bundleIdentifier )
2019-07-19 16:42:40 -07:00
cell ? . mode = . expanded
2019-06-05 11:03:49 -07:00
}
2023-03-01 00:48:36 -05:00
cachedUpdateSizes [ installedApp . bundleIdentifier ] = nil
collectionView . performBatchUpdates ( {
2019-07-19 16:42:40 -07:00
self . collectionView . collectionViewLayout . invalidateLayout ( )
} , completion : nil )
}
2023-03-01 00:48:36 -05:00
@IBAction func refreshApp ( _ sender : UIButton ) {
let point = collectionView . convert ( sender . center , from : sender . superview )
guard let indexPath = collectionView . indexPathForItem ( at : point ) else { return }
let installedApp = dataSource . item ( at : indexPath )
refresh ( installedApp )
}
@IBAction func refreshAllApps ( _ : UIBarButtonItem ) {
isRefreshingAllApps = true
collectionView . collectionViewLayout . invalidateLayout ( )
2019-07-19 16:42:40 -07:00
let installedApps = InstalledApp . fetchAppsForRefreshingAll ( in : DatabaseManager . shared . viewContext )
2023-03-01 00:48:36 -05:00
refresh ( installedApps ) { _ in
2019-07-19 16:42:40 -07:00
DispatchQueue . main . async {
self . isRefreshingAllApps = false
2020-03-11 14:43:19 -07:00
self . collectionView . reloadSections ( [ Section . activeApps . rawValue , Section . inactiveApps . rawValue ] )
2019-07-19 16:42:40 -07:00
}
}
2023-03-01 00:48:36 -05:00
if #available ( iOS 14 , * ) {
2020-09-08 12:29:44 -07:00
let interaction = INInteraction . refreshAllApps ( )
2023-03-01 00:48:36 -05:00
interaction . donate { error in
2020-09-08 12:29:44 -07:00
guard let error = error else { return }
2023-03-02 00:40:11 -05:00
os_log ( " Failed to donate intent %@ . %@ " , type : . error , interaction . intent , error . localizedDescription )
2020-09-08 12:29:44 -07:00
}
}
2019-05-20 21:36:39 +02:00
}
2023-03-01 00:48:36 -05:00
@IBAction func updateApp ( _ sender : UIButton ) {
let point = collectionView . convert ( sender . center , from : sender . superview )
guard let indexPath = collectionView . indexPathForItem ( at : point ) else { return }
let installedApp = dataSource . item ( at : indexPath )
2020-03-31 14:31:34 -07:00
let previousProgress = AppManager . shared . installationProgress ( for : installedApp )
2019-07-19 16:42:40 -07:00
guard previousProgress = = nil else {
previousProgress ? . cancel ( )
return
}
2023-03-01 00:48:36 -05:00
_ = AppManager . shared . update ( installedApp , presentingViewController : self ) { result in
2019-07-19 16:42:40 -07:00
DispatchQueue . main . async {
2023-03-01 00:48:36 -05:00
switch result {
2019-07-19 16:42:40 -07:00
case . failure ( OperationError . cancelled ) :
self . collectionView . reloadItems ( at : [ indexPath ] )
2023-03-01 00:48:36 -05:00
case let . failure ( error ) :
2020-01-24 14:14:08 -08:00
let toastView = ToastView ( error : error )
toastView . show ( in : self )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
self . collectionView . reloadItems ( at : [ indexPath ] )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
case . success :
2023-03-02 00:40:11 -05:00
os_log ( " Updated app: %@ " , type : . info , installedApp . bundleIdentifier )
2019-07-19 16:42:40 -07:00
// N o n e e d t o r e l o a d , s i n c e t h e t h e u p d a t e c e l l i s g o n e n o w .
}
2023-03-01 00:48:36 -05:00
2019-07-24 15:01:33 -07:00
self . update ( )
2019-07-19 16:42:40 -07:00
}
}
2023-03-01 00:48:36 -05:00
collectionView . reloadItems ( at : [ indexPath ] )
2019-07-19 16:42:40 -07:00
}
2023-03-01 00:48:36 -05:00
@IBAction func sideloadApp ( _ : UIBarButtonItem ) {
2020-03-30 13:25:14 -07:00
let supportedTypes : [ String ]
2023-03-01 00:48:36 -05:00
if let types = UTTypeCreateAllIdentifiersForTag ( kUTTagClassFilenameExtension , " ipa " as CFString , nil ) ? . takeRetainedValue ( ) {
2020-03-30 13:25:14 -07:00
supportedTypes = ( types as NSArray ) . map { $0 as ! String }
2023-03-01 00:48:36 -05:00
} else {
2020-03-30 13:25:14 -07:00
supportedTypes = [ " com.apple.itunes.ipa " ] // D e c l a r e d b y t h e s y s t e m .
}
2023-03-01 00:48:36 -05:00
2020-03-30 13:25:14 -07:00
let documentPickerViewController = UIDocumentPickerViewController ( documentTypes : supportedTypes , in : . import )
documentPickerViewController . delegate = self
2023-03-01 00:48:36 -05:00
present ( documentPickerViewController , animated : true , completion : nil )
2019-07-28 15:51:36 -07:00
}
2023-03-01 00:48:36 -05:00
func sideloadApp ( at url : URL , completion : @ escaping ( Result < Void , Error > ) -> Void ) {
2020-05-17 23:44:36 -07:00
let progress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
navigationItem . leftBarButtonItem ? . isIndicatingActivity = true
class Context {
2020-05-17 23:44:36 -07:00
var fileURL : URL ?
var application : ALTApplication ?
var installedApp : InstalledApp ? {
didSet {
2023-03-01 00:48:36 -05:00
installedAppContext = installedApp ? . managedObjectContext
2020-05-17 23:44:36 -07:00
}
}
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
private var installedAppContext : NSManagedObjectContext ?
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
var error : Error ?
}
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
let temporaryDirectory = FileManager . default . uniqueTemporaryURL ( )
let unzippedAppDirectory = temporaryDirectory . appendingPathComponent ( " App " )
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
let context = Context ( )
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
let downloadOperation : RSTAsyncBlockOperation ?
2023-03-01 00:48:36 -05:00
if url . isFileURL {
2020-05-17 23:44:36 -07:00
downloadOperation = nil
context . fileURL = url
progress . totalUnitCount -= 20
2023-03-01 00:48:36 -05:00
} else {
2020-05-17 23:44:36 -07:00
let downloadProgress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
downloadOperation = RSTAsyncBlockOperation { operation in
let downloadTask = URLSession . shared . downloadTask ( with : url ) { fileURL , response , error in
do {
2020-05-17 23:44:36 -07:00
let ( fileURL , _ ) = try Result ( ( fileURL , response ) , error ) . get ( )
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
try FileManager . default . createDirectory ( at : temporaryDirectory , withIntermediateDirectories : true , attributes : nil )
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
let destinationURL = temporaryDirectory . appendingPathComponent ( " App.ipa " )
try FileManager . default . moveItem ( at : fileURL , to : destinationURL )
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
context . fileURL = destinationURL
2023-03-01 00:48:36 -05:00
} catch {
2020-05-17 23:44:36 -07:00
context . error = error
}
operation . finish ( )
2020-03-20 15:56:10 -07:00
}
2020-05-17 23:44:36 -07:00
downloadProgress . addChild ( downloadTask . progress , withPendingUnitCount : 100 )
downloadTask . resume ( )
2020-03-20 15:56:10 -07:00
}
2020-05-17 23:44:36 -07:00
progress . addChild ( downloadProgress , withPendingUnitCount : 20 )
2020-03-20 15:56:10 -07:00
}
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
let unzipProgress = Progress . discreteProgress ( totalUnitCount : 1 )
let unzipAppOperation = BlockOperation {
2023-03-01 00:48:36 -05:00
do {
if let error = context . error {
2020-05-17 23:44:36 -07:00
throw error
}
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
guard let fileURL = context . fileURL else { throw OperationError . invalidParameters }
2021-10-25 21:46:19 -07:00
defer {
try ? FileManager . default . removeItem ( at : fileURL )
}
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
try FileManager . default . createDirectory ( at : unzippedAppDirectory , withIntermediateDirectories : true , attributes : nil )
let unzippedApplicationURL = try FileManager . default . unzipAppBundle ( at : fileURL , toDirectory : unzippedAppDirectory )
2023-03-01 00:48:36 -05:00
2020-03-20 15:56:10 -07:00
guard let application = ALTApplication ( fileURL : unzippedApplicationURL ) else { throw OperationError . invalidApp }
2020-05-17 23:44:36 -07:00
context . application = application
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
unzipProgress . completedUnitCount = 1
2023-03-01 00:48:36 -05:00
} catch {
2020-05-17 23:44:36 -07:00
context . error = error
}
}
progress . addChild ( unzipProgress , withPendingUnitCount : 10 )
2023-03-01 00:48:36 -05:00
if let downloadOperation = downloadOperation {
2020-05-17 23:44:36 -07:00
unzipAppOperation . addDependency ( downloadOperation )
}
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
let removeAppExtensionsProgress = Progress . discreteProgress ( totalUnitCount : 1 )
2023-03-01 00:48:36 -05:00
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [ weak self ] operation in
do {
if let error = context . error {
2020-05-17 23:44:36 -07:00
throw error
2019-11-05 13:26:01 -08:00
}
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
guard let application = context . application else { throw OperationError . invalidParameters }
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
DispatchQueue . main . async {
2023-03-01 00:48:36 -05:00
self ? . removeAppExtensions ( from : application ) { result in
switch result {
2020-05-17 23:44:36 -07:00
case . success : removeAppExtensionsProgress . completedUnitCount = 1
2023-03-01 00:48:36 -05:00
case let . failure ( error ) : context . error = error
2020-05-17 23:44:36 -07:00
}
operation . finish ( )
2020-03-20 15:56:10 -07:00
}
}
2023-03-01 00:48:36 -05:00
} catch {
2020-05-17 23:44:36 -07:00
context . error = error
operation . finish ( )
}
}
removeAppExtensionsOperation . addDependency ( unzipAppOperation )
progress . addChild ( removeAppExtensionsProgress , withPendingUnitCount : 5 )
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
let installProgress = Progress . discreteProgress ( totalUnitCount : 100 )
2023-03-01 00:48:36 -05:00
let installAppOperation = RSTAsyncBlockOperation { operation in
do {
if let error = context . error {
2020-05-17 23:44:36 -07:00
throw error
2020-03-20 15:56:10 -07:00
}
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
guard let application = context . application else { throw OperationError . invalidParameters }
2023-03-01 00:48:36 -05:00
let group = AppManager . shared . install ( application , presentingViewController : self ) { result in
switch result {
case let . success ( installedApp ) : context . installedApp = installedApp
case let . failure ( error ) : context . error = error
2020-05-17 23:44:36 -07:00
}
operation . finish ( )
}
2021-10-25 22:27:30 -07:00
installProgress . addChild ( group . progress , withPendingUnitCount : 100 )
2023-03-01 00:48:36 -05:00
} catch {
2020-05-17 23:44:36 -07:00
context . error = error
operation . finish ( )
}
}
installAppOperation . completionBlock = {
try ? FileManager . default . removeItem ( at : temporaryDirectory )
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
DispatchQueue . main . async {
self . navigationItem . leftBarButtonItem ? . isIndicatingActivity = false
self . sideloadingProgressView . observedProgress = nil
self . sideloadingProgressView . setHidden ( true , animated : true )
2023-03-01 00:48:36 -05:00
switch Result ( context . installedApp , context . error ) {
case let . success ( app ) :
2020-05-17 23:44:36 -07:00
completion ( . success ( ( ) ) )
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
app . managedObjectContext ? . perform {
2023-03-02 00:40:11 -05:00
os_log ( " Successfully installed app: %@ " , type : . info , app . bundleIdentifier )
2020-05-17 23:44:36 -07:00
}
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
case . failure ( OperationError . cancelled ) :
2023-03-01 00:48:36 -05:00
completion ( . failure ( OperationError . cancelled ) )
case let . failure ( error ) :
2020-05-17 23:44:36 -07:00
let toastView = ToastView ( error : error )
toastView . show ( in : self )
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
completion ( . failure ( error ) )
}
2019-09-27 17:39:36 -07:00
}
}
2020-05-17 23:44:36 -07:00
progress . addChild ( installProgress , withPendingUnitCount : 65 )
installAppOperation . addDependency ( removeAppExtensionsOperation )
2023-03-01 00:48:36 -05:00
sideloadingProgress = progress
sideloadingProgressView . progress = 0
sideloadingProgressView . isHidden = false
sideloadingProgressView . observedProgress = sideloadingProgress
2020-05-17 23:44:36 -07:00
let operations = [ downloadOperation , unzipAppOperation , removeAppExtensionsOperation , installAppOperation ] . compactMap { $0 }
2023-03-01 00:48:36 -05:00
operationQueue . addOperations ( operations , waitUntilFinished : false )
2019-09-27 17:39:36 -07:00
}
2023-03-01 00:48:36 -05:00
@IBAction func activateApp ( _ sender : UIButton ) {
let point = collectionView . convert ( sender . center , from : sender . superview )
guard let indexPath = collectionView . indexPathForItem ( at : point ) else { return }
let installedApp = dataSource . item ( at : indexPath )
activate ( installedApp )
}
@IBAction func deactivateApp ( _ sender : UIButton ) {
let point = collectionView . convert ( sender . center , from : sender . superview )
guard let indexPath = collectionView . indexPathForItem ( at : point ) else { return }
let installedApp = dataSource . item ( at : indexPath )
deactivate ( installedApp )
}
@objc func presentInactiveAppsAlert ( ) {
2020-05-17 23:36:30 -07:00
let message : String
2023-03-01 00:48:36 -05:00
if UserDefaults . standard . activeAppLimitIncludesExtensions {
2021-10-04 15:29:10 -07:00
message = NSLocalizedString ( " Non-developer Apple IDs are limited to 3 apps and app extensions. Inactive apps don't count towards your total, but cannot be opened until activated. " , comment : " " )
2023-03-01 00:48:36 -05:00
} else {
2021-10-04 15:29:10 -07:00
message = NSLocalizedString ( " Non-developer Apple IDs are limited to 3 apps. Inactive apps are backed up and uninstalled so they don't count towards your total, but will be reinstalled with all their data when activated again. " , comment : " " )
2020-05-17 23:36:30 -07:00
}
2023-03-01 00:48:36 -05:00
2020-05-17 23:36:30 -07:00
let alertController = UIAlertController ( title : NSLocalizedString ( " What are inactive apps? " , comment : " " ) , message : message , preferredStyle : . alert )
2020-03-11 14:43:19 -07:00
alertController . addAction ( . ok )
2023-03-01 00:48:36 -05:00
present ( alertController , animated : true , completion : nil )
2020-03-11 14:43:19 -07:00
}
2023-03-01 00:48:36 -05:00
func updateCell ( at indexPath : IndexPath ) {
2020-03-20 16:32:31 -07:00
guard let cell = collectionView . cellForItem ( at : indexPath ) as ? InstalledAppCollectionViewCell else { return }
2023-03-01 00:48:36 -05:00
let installedApp = dataSource . item ( at : indexPath )
dataSource . cellConfigurationHandler ( cell , installedApp , indexPath )
2020-03-20 16:32:31 -07:00
cell . bannerView . iconImageView . isIndicatingActivity = false
2020-03-11 14:43:19 -07:00
}
2023-03-01 00:48:36 -05:00
func removeAppExtensions ( from application : ALTApplication , completion : @ escaping ( Result < Void , Error > ) -> Void ) {
2020-05-17 23:44:36 -07:00
guard ! application . appExtensions . isEmpty else { return completion ( . success ( ( ) ) ) }
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
let firstSentence : String
2023-03-01 00:48:36 -05:00
if UserDefaults . standard . activeAppLimitIncludesExtensions {
2021-10-04 15:29:10 -07:00
firstSentence = NSLocalizedString ( " Non-developer Apple IDs are limited to 3 active apps and app extensions. " , comment : " " )
2023-03-01 00:48:36 -05:00
} else {
2021-10-04 15:29:10 -07:00
firstSentence = NSLocalizedString ( " Non-developer Apple IDs are limited to creating 10 App IDs per week. " , comment : " " )
2020-05-17 23:44:36 -07:00
}
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
let message = firstSentence + " " + NSLocalizedString ( " Would you like to remove this app's extensions so they don't count towards your limit? " , comment : " " )
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
let alertController = UIAlertController ( title : NSLocalizedString ( " App Contains Extensions " , comment : " " ) , message : message , preferredStyle : . alert )
2023-03-01 00:48:36 -05:00
alertController . addAction ( UIAlertAction ( title : UIAlertAction . cancel . title , style : UIAlertAction . cancel . style , handler : { _ in
2020-05-17 23:44:36 -07:00
completion ( . failure ( OperationError . cancelled ) )
} ) )
2023-03-01 00:48:36 -05:00
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Keep App Extensions " , comment : " " ) , style : . default ) { _ in
2020-05-17 23:44:36 -07:00
completion ( . success ( ( ) ) )
} )
2023-03-01 00:48:36 -05:00
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Remove App Extensions " , comment : " " ) , style : . destructive ) { _ in
do {
for appExtension in application . appExtensions {
2020-05-17 23:44:36 -07:00
try FileManager . default . removeItem ( at : appExtension . fileURL )
}
2023-03-01 00:48:36 -05:00
2020-05-17 23:44:36 -07:00
completion ( . success ( ( ) ) )
2023-03-01 00:48:36 -05:00
} catch {
2020-05-17 23:44:36 -07:00
completion ( . failure ( error ) )
}
} )
2023-03-01 00:48:36 -05:00
present ( alertController , animated : true , completion : nil )
2020-05-17 23:44:36 -07:00
}
2020-03-11 14:43:19 -07:00
}
2023-03-01 00:48:36 -05:00
private extension MyAppsViewController {
func open ( _ installedApp : InstalledApp ) {
2021-09-02 15:52:59 -05:00
UIApplication . shared . open ( installedApp . openAppURL ) { success in
guard ! success else { return }
2023-03-01 00:48:36 -05:00
2021-09-02 15:52:59 -05:00
let toastView = ToastView ( error : OperationError . openAppFailed ( name : installedApp . name ) )
toastView . show ( in : self )
}
}
2023-03-01 00:48:36 -05:00
func refresh ( _ installedApp : InstalledApp ) {
2020-03-11 14:43:19 -07:00
let previousProgress = AppManager . shared . refreshProgress ( for : installedApp )
guard previousProgress = = nil else {
previousProgress ? . cancel ( )
return
}
2023-03-01 00:48:36 -05:00
refresh ( [ installedApp ] ) { results in
2020-03-11 14:43:19 -07:00
// I f a n e r r o r o c c u r e d , r e l o a d t h e s e c t i o n s o t h e p r o g r e s s b a r i s n o l o n g e r v i s i b l e .
2023-03-01 00:48:36 -05:00
if results . values . contains ( where : { $0 . error != nil } ) {
2020-03-11 14:43:19 -07:00
DispatchQueue . main . async {
self . collectionView . reloadSections ( [ Section . activeApps . rawValue , Section . inactiveApps . rawValue ] )
}
}
2023-03-01 00:48:36 -05:00
2023-03-02 00:40:11 -05:00
let errors = results . filter ( { $1 . error != nil } ) . map { ( $0 , $1 . error ? . localizedDescription ? ? " no description " ) }
let successes = results . filter ( { $1 . error = = nil } ) . map { ( $0 , " success " ) }
if ! errors . isEmpty {
os_log ( " Finished refreshing Errors: %@ " , type : . error , errors . map { " \( $0 . 0 ) - \( $0 . 1 ) " } . joined ( separator : " \n " ) )
}
if ! successes . isEmpty {
os_log ( " Finished refreshing success: %@ " , type : . info , successes . map { " \( $0 . 0 ) - \( $0 . 1 ) " } . joined ( separator : " \n " ) )
}
2020-03-11 14:43:19 -07:00
}
}
2023-03-01 00:48:36 -05:00
func activate ( _ installedApp : InstalledApp ) {
func finish ( _ result : Result < InstalledApp , Error > ) {
do {
2021-09-15 14:27:16 -07:00
let app = try result . get ( )
app . managedObjectContext ? . perform {
2020-03-20 16:32:31 -07:00
try ? app . managedObjectContext ? . save ( )
}
2023-03-01 00:48:36 -05:00
} catch OperationError . cancelled {
2021-09-15 14:27:16 -07:00
// I g n o r e
2023-03-01 00:48:36 -05:00
} catch {
2023-03-02 00:40:11 -05:00
os_log ( " Failed to activate app: %@ " , type : . error , error . localizedDescription )
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
DispatchQueue . main . async {
installedApp . isActive = false
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
let toastView = ToastView ( error : error )
toastView . show ( in : self )
2020-03-11 14:43:19 -07:00
}
}
}
2023-03-01 00:48:36 -05:00
if UserDefaults . standard . activeAppsLimit != nil , #available ( iOS 13 , * ) {
2021-09-15 14:27:16 -07:00
// U s e r D e f a u l t s . s t a n d a r d . a c t i v e A p p s L i m i t i s o n l y n o n - n i l o n i O S 1 3 . 3 . 1 o r l a t e r , s o t h e # a v a i l a b l e c h e c k i s j u s t s o w e c a n u s e C o m b i n e .
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
guard let app = ALTApplication ( fileURL : installedApp . fileURL ) else { return finish ( . failure ( OperationError . invalidApp ) ) }
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
var cancellable : AnyCancellable ?
cancellable = DatabaseManager . shared . viewContext . registeredObjects . publisher
. compactMap { $0 as ? InstalledApp }
. filter ( \ . isActive )
. map { $0 . publisher ( for : \ . isActive ) }
. collect ( )
. flatMap { publishers in
Publishers . MergeMany ( publishers )
2020-03-20 16:32:31 -07:00
}
2021-09-15 14:27:16 -07:00
. first { isActive in ! isActive }
. sink { _ in
// A p r e v i o u s l y a c t i v e a p p i s n o w i n a c t i v e ,
// w h i c h m e a n s t h e r e a r e n o w e n o u g h s l o t s t o a c t i v a t e t h e a p p ,
// s o p r e - e m p t i v e l y m a r k i t a s a c t i v e t o p r o v i d e v i s u a l f e e d b a c k s o o n e r .
installedApp . isActive = true
cancellable ? . cancel ( )
}
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
AppManager . shared . deactivateApps ( for : app , presentingViewController : self ) { result in
cancellable ? . cancel ( )
installedApp . managedObjectContext ? . perform {
2023-03-01 00:48:36 -05:00
switch result {
case let . failure ( error ) :
2021-09-15 14:27:16 -07:00
installedApp . isActive = false
finish ( . failure ( error ) )
2023-03-01 00:48:36 -05:00
2021-09-15 14:27:16 -07:00
case . success :
installedApp . isActive = true
AppManager . shared . activate ( installedApp , presentingViewController : self , completionHandler : finish ( _ : ) )
}
2020-03-11 14:43:19 -07:00
}
}
2023-03-01 00:48:36 -05:00
} else {
2021-09-15 14:27:16 -07:00
installedApp . isActive = true
AppManager . shared . activate ( installedApp , presentingViewController : self , completionHandler : finish ( _ : ) )
2020-03-20 16:32:31 -07:00
}
2020-03-11 14:43:19 -07:00
}
2023-03-01 00:48:36 -05:00
func deactivate ( _ installedApp : InstalledApp , completionHandler : ( ( Result < InstalledApp , Error > ) -> Void ) ? = nil ) {
2020-03-11 14:43:19 -07:00
guard installedApp . isActive else { return }
installedApp . isActive = false
2023-03-01 00:48:36 -05:00
AppManager . shared . deactivate ( installedApp , presentingViewController : self ) { result in
do {
2020-03-11 14:43:19 -07:00
let app = try result . get ( )
try ? app . managedObjectContext ? . save ( )
2023-03-01 00:48:36 -05:00
2023-03-02 00:40:11 -05:00
os_log ( " Finished deactivating app: %@ " , type : . info , app . bundleIdentifier )
2023-03-01 00:48:36 -05:00
} catch {
2023-03-02 00:40:11 -05:00
os_log ( " Failed to activate app: %@ " , type : . error , error . localizedDescription )
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
DispatchQueue . main . async {
installedApp . isActive = true
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
let toastView = ToastView ( error : error )
toastView . show ( in : self )
}
}
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
completionHandler ? ( result )
}
}
2023-03-01 00:48:36 -05:00
func remove ( _ installedApp : InstalledApp ) {
2022-11-05 23:50:07 -07:00
let title = String ( format : NSLocalizedString ( " Remove “%@” from SideStore? " , comment : " " ) , installedApp . name )
2020-05-16 15:34:10 -07:00
let message : String
2023-03-01 00:48:36 -05:00
if UserDefaults . standard . isLegacyDeactivationSupported {
2020-05-16 15:34:10 -07:00
message = NSLocalizedString ( " You must also delete it from the home screen to fully uninstall the app. " , comment : " " )
2023-03-01 00:48:36 -05:00
} else {
2020-05-16 15:34:10 -07:00
message = NSLocalizedString ( " This will also erase all backup data for this app. " , comment : " " )
}
let alertController = UIAlertController ( title : title , message : message , preferredStyle : . actionSheet )
2019-07-28 15:51:36 -07:00
alertController . addAction ( . cancel )
2023-03-01 00:48:36 -05:00
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Remove " , comment : " " ) , style : . destructive , handler : { _ in
AppManager . shared . remove ( installedApp ) { result in
switch result {
2020-05-16 15:34:10 -07:00
case . success : break
2023-03-01 00:48:36 -05:00
case let . failure ( error ) :
2020-05-16 15:34:10 -07:00
DispatchQueue . main . async {
let toastView = ToastView ( error : error )
toastView . show ( in : self )
}
}
2019-07-28 15:51:36 -07:00
}
} ) )
2023-03-01 00:48:36 -05:00
present ( alertController , animated : true , completion : nil )
2019-07-28 15:51:36 -07:00
}
2023-03-01 00:48:36 -05:00
func backup ( _ installedApp : InstalledApp ) {
2020-05-19 11:47:43 -07:00
let title = NSLocalizedString ( " Start Backup? " , comment : " " )
2022-11-05 23:50:07 -07:00
let message = NSLocalizedString ( " This will replace any previous backups. Please leave SideStore open until the backup is complete. " , comment : " " )
2020-05-19 11:47:43 -07:00
let alertController = UIAlertController ( title : title , message : message , preferredStyle : . actionSheet )
alertController . addAction ( . cancel )
2023-03-01 00:48:36 -05:00
2020-05-19 11:47:43 -07:00
let actionTitle = String ( format : NSLocalizedString ( " Back Up %@ " , comment : " " ) , installedApp . name )
2023-03-01 00:48:36 -05:00
alertController . addAction ( UIAlertAction ( title : actionTitle , style : . default , handler : { _ in
AppManager . shared . backup ( installedApp , presentingViewController : self ) { result in
do {
2020-05-19 11:47:43 -07:00
let app = try result . get ( )
try ? app . managedObjectContext ? . save ( )
2023-03-01 00:48:36 -05:00
2023-03-02 00:40:11 -05:00
os_log ( " Finished backing up app: %@ " , type : . info , app . bundleIdentifier )
2023-03-01 00:48:36 -05:00
} catch {
2023-03-02 00:40:11 -05:00
os_log ( " Failed to back up app: %@ " , type : . error , error . localizedDescription )
2023-03-01 00:48:36 -05:00
2020-05-19 11:47:43 -07:00
DispatchQueue . main . async {
let toastView = ToastView ( error : error )
toastView . show ( in : self )
2023-03-01 00:48:36 -05:00
2020-05-19 11:47:43 -07:00
self . collectionView . reloadSections ( [ Section . activeApps . rawValue , Section . inactiveApps . rawValue ] )
}
}
}
2023-03-01 00:48:36 -05:00
2020-05-19 11:47:43 -07:00
DispatchQueue . main . async {
self . collectionView . reloadSections ( [ Section . activeApps . rawValue , Section . inactiveApps . rawValue ] )
}
} ) )
2023-03-01 00:48:36 -05:00
present ( alertController , animated : true , completion : nil )
2020-05-19 11:47:43 -07:00
}
2023-03-01 00:48:36 -05:00
func restore ( _ installedApp : InstalledApp ) {
2020-05-16 16:39:02 -07:00
let message = String ( format : NSLocalizedString ( " This will replace all data you currently have in %@. " , comment : " " ) , installedApp . name )
let alertController = UIAlertController ( title : NSLocalizedString ( " Are you sure you want to restore this backup? " , comment : " " ) , message : message , preferredStyle : . actionSheet )
alertController . addAction ( . cancel )
2023-03-01 00:48:36 -05:00
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Restore Backup " , comment : " " ) , style : . destructive , handler : { _ in
AppManager . shared . restore ( installedApp , presentingViewController : self ) { result in
do {
2020-05-16 16:39:02 -07:00
let app = try result . get ( )
try ? app . managedObjectContext ? . save ( )
2023-03-01 00:48:36 -05:00
2023-03-02 00:40:11 -05:00
os_log ( " Finished restoring app: %@ " , type : . info , app . bundleIdentifier )
2023-03-01 00:48:36 -05:00
} catch {
2023-03-02 00:40:11 -05:00
os_log ( " Failed to restore app: %@ " , type : . error , error . localizedDescription )
2023-03-01 00:48:36 -05:00
2020-05-16 16:39:02 -07:00
DispatchQueue . main . async {
let toastView = ToastView ( error : error )
toastView . show ( in : self )
}
}
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:39:02 -07:00
DispatchQueue . main . async {
self . collectionView . reloadSections ( [ Section . activeApps . rawValue ] )
}
} ) )
2023-03-01 00:48:36 -05:00
present ( alertController , animated : true , completion : nil )
2020-05-16 16:39:02 -07:00
}
2023-03-01 00:48:36 -05:00
func exportBackup ( for installedApp : InstalledApp ) {
2020-05-16 16:34:50 -07:00
guard let backupURL = FileManager . default . backupDirectoryURL ( for : installedApp ) else { return }
2023-03-01 00:48:36 -05:00
2020-05-16 16:34:50 -07:00
let documentPicker = UIDocumentPickerViewController ( url : backupURL , in : . exportToService )
documentPicker . delegate = self
2023-03-01 00:48:36 -05:00
present ( documentPicker , animated : true , completion : nil )
2020-05-16 16:34:50 -07:00
}
2023-03-01 00:48:36 -05:00
func chooseIcon ( for installedApp : InstalledApp ) {
_imagePickerInstalledApp = installedApp
2020-10-01 14:09:45 -07:00
let imagePicker = UIImagePickerController ( )
imagePicker . delegate = self
imagePicker . allowsEditing = true
2023-03-01 00:48:36 -05:00
present ( imagePicker , animated : true , completion : nil )
2020-10-01 14:09:45 -07:00
}
2023-03-01 00:48:36 -05:00
func changeIcon ( for installedApp : InstalledApp , to image : UIImage ? ) {
2020-10-01 14:09:45 -07:00
// R e m o v e p r e v i o u s i c o n f r o m c a c h e .
2023-03-01 00:48:36 -05:00
activeAppsDataSource . prefetchItemCache . removeObject ( forKey : installedApp )
inactiveAppsDataSource . prefetchItemCache . removeObject ( forKey : installedApp )
DatabaseManager . shared . persistentContainer . performBackgroundTask { context in
do {
2020-10-01 14:09:45 -07:00
let tempApp = context . object ( with : installedApp . objectID ) as ! InstalledApp
tempApp . needsResign = true
tempApp . hasAlternateIcon = ( image != nil )
2023-03-01 00:48:36 -05:00
if let image = image {
2020-10-01 14:09:45 -07:00
guard let icon = image . resizing ( toFill : CGSize ( width : 256 , height : 256 ) ) ,
let iconData = icon . pngData ( )
else { return }
2023-03-01 00:48:36 -05:00
2020-10-01 14:09:45 -07:00
try iconData . write ( to : tempApp . alternateIconURL , options : . atomic )
2023-03-01 00:48:36 -05:00
} else {
2020-10-01 14:09:45 -07:00
try FileManager . default . removeItem ( at : tempApp . alternateIconURL )
}
2023-03-01 00:48:36 -05:00
2020-10-01 14:09:45 -07:00
try context . save ( )
2023-03-01 00:48:36 -05:00
if tempApp . isActive {
2020-10-01 14:09:45 -07:00
DispatchQueue . main . async {
self . refresh ( installedApp )
}
}
2023-03-01 00:48:36 -05:00
} catch {
2023-03-02 00:40:11 -05:00
os_log ( " Failed to change app icon. %@ " , type : . error , error . localizedDescription )
2023-03-01 00:48:36 -05:00
2020-10-01 14:09:45 -07:00
DispatchQueue . main . async {
let toastView = ToastView ( error : error )
toastView . show ( in : self )
}
}
}
}
2023-03-01 00:48:36 -05:00
2021-09-03 13:57:15 -05:00
@ available ( iOS 14 , * )
2023-03-01 00:48:36 -05:00
func enableJIT ( for installedApp : InstalledApp ) {
2021-09-03 13:57:15 -05:00
AppManager . shared . enableJIT ( for : installedApp ) { result in
DispatchQueue . main . async {
2023-03-01 00:48:36 -05:00
switch result {
2021-09-03 13:57:15 -05:00
case . success : break
2023-03-01 00:48:36 -05:00
case let . failure ( error ) :
2021-09-03 13:57:15 -05:00
let toastView = ToastView ( error : error )
toastView . show ( in : self )
}
}
}
}
2019-07-28 15:51:36 -07:00
}
2023-03-01 00:48:36 -05:00
private extension MyAppsViewController {
@objc func didFetchSource ( _ : Notification ) {
2019-07-19 16:42:40 -07:00
DispatchQueue . main . async {
2023-03-01 00:48:36 -05:00
if self . updatesDataSource . fetchedResultsController . fetchedObjects = = nil {
2023-03-02 00:40:11 -05:00
do { try self . updatesDataSource . fetchedResultsController . performFetch ( ) } catch { os_log ( " Error fetching: %@ " , type : . error , error . localizedDescription ) }
2019-07-19 16:42:40 -07:00
}
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
self . update ( )
}
}
2023-03-01 00:48:36 -05:00
@objc func importApp ( _ notification : Notification ) {
2020-02-11 13:29:28 -08:00
// M a k e s u r e l e f t U I B a r B u t t o n I t e m h a s b e e n s e t .
2023-03-01 00:48:36 -05:00
loadViewIfNeeded ( )
2023-03-01 19:09:33 -05:00
guard let url = notification . userInfo ? [ SideStoreAppDelegate . importAppDeepLinkURLKey ] as ? URL else { return }
2023-03-01 00:48:36 -05:00
sideloadApp ( at : url ) { _ in
2020-05-17 23:44:36 -07:00
guard url . isFileURL else { return }
2023-03-01 00:48:36 -05:00
do {
2020-05-17 23:44:36 -07:00
try FileManager . default . removeItem ( at : url )
2023-03-01 00:48:36 -05:00
} catch {
2023-03-02 00:40:11 -05:00
os_log ( " Unable to remove imported .ipa. %@ " , type : . error , error . localizedDescription )
2019-09-27 17:39:36 -07:00
}
}
}
2019-07-19 16:42:40 -07:00
}
2023-03-01 00:48:36 -05:00
extension MyAppsViewController {
override func collectionView ( _ collectionView : UICollectionView , viewForSupplementaryElementOfKind kind : String , at indexPath : IndexPath ) -> UICollectionReusableView {
2019-07-31 13:37:51 -07:00
let section = Section ( rawValue : indexPath . section ) !
2023-03-01 00:48:36 -05:00
switch section {
2019-07-31 13:37:51 -07:00
case . noUpdates : return UICollectionReusableView ( )
case . updates :
2019-07-19 16:42:40 -07:00
let headerView = collectionView . dequeueReusableSupplementaryView ( ofKind : UICollectionView . elementKindSectionHeader , withReuseIdentifier : " UpdatesHeader " , for : indexPath ) as ! UpdatesCollectionHeaderView
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
UIView . performWithoutAnimation {
2019-09-19 11:29:10 -07:00
headerView . button . backgroundColor = UIColor . altPrimary . withAlphaComponent ( 0.15 )
2019-07-19 16:42:40 -07:00
headerView . button . setTitle ( " ▾ " , for : . normal )
headerView . button . titleLabel ? . font = UIFont . boldSystemFont ( ofSize : 28 )
2019-09-19 11:29:10 -07:00
headerView . button . setTitleColor ( . altPrimary , for : . normal )
2019-07-19 16:42:40 -07:00
headerView . button . addTarget ( self , action : #selector ( MyAppsViewController . toggleAppUpdates ) , for : . primaryActionTriggered )
2023-03-01 00:48:36 -05:00
if self . isUpdateSectionCollapsed {
2019-07-24 15:01:33 -07:00
headerView . button . titleLabel ? . transform = . identity
2023-03-01 00:48:36 -05:00
} else {
2019-07-24 15:01:33 -07:00
headerView . button . titleLabel ? . transform = CGAffineTransform . identity . rotated ( by : . pi )
}
2023-03-01 00:48:36 -05:00
2019-07-24 15:01:33 -07:00
headerView . isHidden = ( self . updatesDataSource . itemCount <= 2 )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
headerView . button . layoutIfNeeded ( )
}
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
return headerView
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
case . activeApps where kind = = UICollectionView . elementKindSectionHeader :
let headerView = collectionView . dequeueReusableSupplementaryView ( ofKind : UICollectionView . elementKindSectionHeader , withReuseIdentifier : " ActiveAppsHeader " , for : indexPath ) as ! InstalledAppsCollectionHeaderView
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
UIView . performWithoutAnimation {
2020-03-11 14:43:19 -07:00
headerView . layoutMargins . left = self . view . layoutMargins . left
headerView . layoutMargins . right = self . view . layoutMargins . right
2023-03-01 00:48:36 -05:00
if UserDefaults . standard . activeAppsLimit = = nil {
2020-03-11 14:43:19 -07:00
headerView . textLabel . text = NSLocalizedString ( " Installed " , comment : " " )
2023-03-01 00:48:36 -05:00
} else {
2020-03-11 14:43:19 -07:00
headerView . textLabel . text = NSLocalizedString ( " Active " , comment : " " )
}
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
headerView . button . isIndicatingActivity = false
2019-09-19 11:29:10 -07:00
headerView . button . activityIndicatorView . color = . altPrimary
2019-07-19 16:42:40 -07:00
headerView . button . setTitle ( NSLocalizedString ( " Refresh All " , comment : " " ) , for : . normal )
headerView . button . addTarget ( self , action : #selector ( MyAppsViewController . refreshAllApps ( _ : ) ) , for : . primaryActionTriggered )
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
headerView . button . layoutIfNeeded ( )
2023-03-01 00:48:36 -05:00
if self . isRefreshingAllApps {
2020-08-27 15:25:52 -07:00
headerView . button . isIndicatingActivity = true
headerView . button . accessibilityLabel = NSLocalizedString ( " Refreshing " , comment : " " )
headerView . button . accessibilityTraits . remove ( . notEnabled )
2023-03-01 00:48:36 -05:00
} else {
2020-08-27 15:25:52 -07:00
headerView . button . isIndicatingActivity = false
headerView . button . accessibilityLabel = nil
}
2020-03-11 14:43:19 -07:00
}
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
return headerView
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
case . inactiveApps where kind = = UICollectionView . elementKindSectionHeader :
let headerView = collectionView . dequeueReusableSupplementaryView ( ofKind : UICollectionView . elementKindSectionHeader , withReuseIdentifier : " InactiveAppsHeader " , for : indexPath ) as ! InstalledAppsCollectionHeaderView
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
UIView . performWithoutAnimation {
headerView . layoutMargins . left = self . view . layoutMargins . left
headerView . layoutMargins . right = self . view . layoutMargins . right
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
headerView . textLabel . text = NSLocalizedString ( " Inactive " , comment : " " )
headerView . button . setTitle ( nil , for : . normal )
2023-03-01 00:48:36 -05:00
if #available ( iOS 13.0 , * ) {
2020-03-11 14:43:19 -07:00
headerView . button . setImage ( UIImage ( systemName : " questionmark.circle " ) , for : . normal )
}
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
headerView . button . addTarget ( self , action : #selector ( MyAppsViewController . presentInactiveAppsAlert ) , for : . primaryActionTriggered )
2019-07-19 16:42:40 -07:00
}
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
return headerView
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
case . activeApps , . inactiveApps :
2020-01-24 14:54:52 -08:00
let footerView = collectionView . dequeueReusableSupplementaryView ( ofKind : UICollectionView . elementKindSectionFooter , withReuseIdentifier : " InstalledAppsFooter " , for : indexPath ) as ! InstalledAppsCollectionFooterView
2023-03-01 00:48:36 -05:00
2020-02-10 17:30:11 -08:00
guard let team = DatabaseManager . shared . activeTeam ( ) else { return footerView }
2023-03-01 00:48:36 -05:00
switch team . type {
2020-02-10 17:30:11 -08:00
case . free :
let registeredAppIDs = team . appIDs . count
2023-03-01 00:48:36 -05:00
2020-02-10 17:30:11 -08:00
let maximumAppIDCount = 10
let remainingAppIDs = max ( maximumAppIDCount - registeredAppIDs , 0 )
2023-03-01 00:48:36 -05:00
if remainingAppIDs = = 1 {
2020-02-10 17:30:11 -08:00
footerView . textLabel . text = String ( format : NSLocalizedString ( " 1 App ID Remaining " , comment : " " ) )
2023-03-01 00:48:36 -05:00
} else {
2020-02-10 17:30:11 -08:00
footerView . textLabel . text = String ( format : NSLocalizedString ( " %@ App IDs Remaining " , comment : " " ) , NSNumber ( value : remainingAppIDs ) )
}
2023-03-01 00:48:36 -05:00
2020-02-10 17:30:11 -08:00
footerView . textLabel . isHidden = false
2023-03-01 00:48:36 -05:00
2020-02-10 17:30:11 -08:00
case . individual , . organization , . unknown : footerView . textLabel . isHidden = true
@ unknown default : break
2020-01-24 14:54:52 -08:00
}
2023-03-01 00:48:36 -05:00
2020-01-24 14:54:52 -08:00
return footerView
2019-07-19 16:42:40 -07:00
}
}
2023-03-01 00:48:36 -05:00
override func collectionView ( _ collectionView : UICollectionView , didSelectItemAt indexPath : IndexPath ) {
2019-09-12 13:51:03 -07:00
let section = Section . allCases [ indexPath . section ]
2023-03-01 00:48:36 -05:00
switch section {
2019-09-12 13:51:03 -07:00
case . updates :
guard let cell = collectionView . cellForItem ( at : indexPath ) else { break }
2023-03-01 00:48:36 -05:00
performSegue ( withIdentifier : " showUpdate " , sender : cell )
2019-09-12 13:51:03 -07:00
default : break
}
}
2019-07-19 16:42:40 -07:00
}
2020-03-11 14:43:19 -07:00
@ available ( iOS 13.0 , * )
2023-03-01 00:48:36 -05:00
extension MyAppsViewController {
private func actions ( for installedApp : InstalledApp ) -> [ UIMenuElement ] {
2020-10-01 14:09:45 -07:00
var actions = [ UIMenuElement ] ( )
2023-03-01 00:48:36 -05:00
let openAction = UIAction ( title : NSLocalizedString ( " Open " , comment : " " ) , image : UIImage ( systemName : " arrow.up.forward.app " ) ) { _ in
2021-09-02 15:52:59 -05:00
self . open ( installedApp )
}
2023-03-01 00:48:36 -05:00
2021-09-02 15:52:59 -05:00
let openMenu = UIMenu ( title : " " , options : . displayInline , children : [ openAction ] )
2023-03-01 00:48:36 -05:00
let refreshAction = UIAction ( title : NSLocalizedString ( " Refresh " , comment : " " ) , image : UIImage ( systemName : " arrow.clockwise " ) ) { _ in
2020-03-11 14:43:19 -07:00
self . refresh ( installedApp )
}
2023-03-01 00:48:36 -05:00
let activateAction = UIAction ( title : NSLocalizedString ( " Activate " , comment : " " ) , image : UIImage ( systemName : " checkmark.circle " ) ) { _ in
2020-03-11 14:43:19 -07:00
self . activate ( installedApp )
}
2023-03-01 00:48:36 -05:00
let deactivateAction = UIAction ( title : NSLocalizedString ( " Deactivate " , comment : " " ) , image : UIImage ( systemName : " xmark.circle " ) , attributes : . destructive ) { _ in
2020-03-11 14:43:19 -07:00
self . deactivate ( installedApp )
}
2023-03-01 00:48:36 -05:00
let removeAction = UIAction ( title : NSLocalizedString ( " Remove " , comment : " " ) , image : UIImage ( systemName : " trash " ) , attributes : . destructive ) { _ in
2020-03-11 14:43:19 -07:00
self . remove ( installedApp )
}
2023-03-01 00:48:36 -05:00
let jitAction = UIAction ( title : NSLocalizedString ( " Enable JIT " , comment : " " ) , image : UIImage ( systemName : " bolt " ) ) { _ in
2021-09-03 13:57:15 -05:00
guard #available ( iOS 14 , * ) else { return }
self . enableJIT ( for : installedApp )
}
2023-03-01 00:48:36 -05:00
let backupAction = UIAction ( title : NSLocalizedString ( " Back Up " , comment : " " ) , image : UIImage ( systemName : " doc.on.doc " ) ) { _ in
2020-05-19 11:47:43 -07:00
self . backup ( installedApp )
}
2023-03-01 00:48:36 -05:00
let exportBackupAction = UIAction ( title : NSLocalizedString ( " Export Backup " , comment : " " ) , image : UIImage ( systemName : " arrow.up.doc " ) ) { _ in
2020-05-16 16:34:50 -07:00
self . exportBackup ( for : installedApp )
}
2023-03-01 00:48:36 -05:00
let restoreBackupAction = UIAction ( title : NSLocalizedString ( " Restore Backup " , comment : " " ) , image : UIImage ( systemName : " arrow.down.doc " ) ) { _ in
2020-05-16 16:39:02 -07:00
self . restore ( installedApp )
}
2023-03-01 00:48:36 -05:00
let chooseIconAction = UIAction ( title : NSLocalizedString ( " Photos " , comment : " " ) , image : UIImage ( systemName : " photo " ) ) { _ in
2020-10-01 14:09:45 -07:00
self . chooseIcon ( for : installedApp )
}
2023-03-01 00:48:36 -05:00
let removeIconAction = UIAction ( title : NSLocalizedString ( " Remove Custom Icon " , comment : " " ) , image : UIImage ( systemName : " trash " ) , attributes : [ . destructive ] ) { _ in
2020-10-01 14:09:45 -07:00
self . changeIcon ( for : installedApp , to : nil )
}
2023-03-01 00:48:36 -05:00
2020-10-01 14:09:45 -07:00
var changeIconActions = [ chooseIconAction ]
2023-03-01 00:48:36 -05:00
if installedApp . hasAlternateIcon {
2020-10-01 14:09:45 -07:00
changeIconActions . append ( removeIconAction )
}
2023-03-01 00:48:36 -05:00
2020-10-01 14:09:45 -07:00
let changeIconMenu = UIMenu ( title : NSLocalizedString ( " Change Icon " , comment : " " ) , image : UIImage ( systemName : " photo " ) , children : changeIconActions )
2023-03-01 00:48:36 -05:00
2020-05-16 15:34:10 -07:00
guard installedApp . bundleIdentifier != StoreApp . altstoreAppID else {
2020-10-05 13:59:44 -07:00
#if BETA
2023-03-01 00:48:36 -05:00
return [ refreshAction , changeIconMenu ]
2020-10-05 13:59:44 -07:00
#else
2023-03-01 00:48:36 -05:00
return [ refreshAction ]
2020-10-05 13:59:44 -07:00
#endif
2020-05-16 15:34:10 -07:00
}
2023-03-01 00:48:36 -05:00
if installedApp . isActive {
2021-09-02 15:52:59 -05:00
actions . append ( openMenu )
2020-05-16 15:34:10 -07:00
actions . append ( refreshAction )
2023-03-01 00:48:36 -05:00
} else {
2020-05-16 15:34:10 -07:00
actions . append ( activateAction )
}
2023-03-01 00:48:36 -05:00
if installedApp . isActive , #available ( iOS 14 , * ) {
2021-09-03 13:57:15 -05:00
actions . append ( jitAction )
}
2023-03-01 00:48:36 -05:00
2020-10-05 13:59:44 -07:00
#if BETA
2023-03-01 00:48:36 -05:00
actions . append ( changeIconMenu )
2020-10-05 13:59:44 -07:00
#endif
2023-03-01 00:48:36 -05:00
if installedApp . isActive {
2020-05-19 11:47:43 -07:00
actions . append ( backupAction )
2023-03-01 00:48:36 -05:00
} else if let _ = UTTypeCopyDeclaration ( installedApp . installedAppUTI as CFString ) ? . takeRetainedValue ( ) as NSDictionary ? , ! UserDefaults . standard . isLegacyDeactivationSupported {
2020-05-19 11:47:43 -07:00
// A l l o w b a c k i n g u p i n a c t i v e a p p s i f t h e y a r e s t i l l i n s t a l l e d ,
// b u t o n a n i O S v e r s i o n t h a t n o l o n g e r s u p p o r t s l e g a c y d e a c t i v a t i o n .
// T h i s h a n d l e s e d g e c a s e w h e r e y o u c a n ' t i n s t a l l m o r e a p p s u n t i l y o u
// d e l e t e s o m e , b u t c a n ' t a c t i v a t e i n a c t i v e a p p s a g a i n t o b a c k t h e m u p f i r s t .
actions . append ( backupAction )
}
2023-03-01 00:48:36 -05:00
if let backupDirectoryURL = FileManager . default . backupDirectoryURL ( for : installedApp ) {
2020-05-16 16:34:50 -07:00
var backupExists = false
2023-03-01 00:48:36 -05:00
var outError : NSError ?
coordinator . coordinate ( readingItemAt : backupDirectoryURL , options : [ . withoutChanges ] , error : & outError ) { backupDirectoryURL in
2020-05-16 16:34:50 -07:00
#if DEBUG
2023-03-01 00:48:36 -05:00
backupExists = true
2020-05-16 16:34:50 -07:00
#else
2023-03-01 00:48:36 -05:00
backupExists = FileManager . default . fileExists ( atPath : backupDirectoryURL . path )
2020-05-16 16:34:50 -07:00
#endif
}
2023-03-01 00:48:36 -05:00
if backupExists {
2020-05-16 16:34:50 -07:00
actions . append ( exportBackupAction )
2023-03-01 00:48:36 -05:00
if installedApp . isActive {
2020-05-16 16:39:02 -07:00
actions . append ( restoreBackupAction )
}
2023-03-01 00:48:36 -05:00
} else if let error = outError {
2023-03-02 00:40:11 -05:00
os_log ( " Unable to check if backup exists: %@ " , type : . error , error . localizedDescription )
2020-05-16 16:34:50 -07:00
}
}
2023-03-01 00:48:36 -05:00
if installedApp . isActive {
2020-05-19 11:47:43 -07:00
actions . append ( deactivateAction )
}
2023-03-01 00:48:36 -05:00
2020-05-16 15:34:10 -07:00
#if DEBUG
2023-03-01 00:48:36 -05:00
if installedApp . bundleIdentifier != StoreApp . altstoreAppID {
actions . append ( removeAction )
}
2020-05-16 15:34:10 -07:00
#else
2023-03-01 00:48:36 -05:00
if ( UserDefaults . standard . legacySideloadedApps ? ? [ ] ) . contains ( installedApp . bundleIdentifier ) {
// L e g a c y s i d e l o a d e d a p p , s o c a n ' t d e t e c t i f i t ' s d e l e t e d .
actions . append ( removeAction )
} else if ! UserDefaults . standard . isLegacyDeactivationSupported , ! installedApp . isActive {
// I n a c t i v e a p p s a r e a c t u a l l y d e l e t e d , s o w e n e e d a n o t h e r w a y
// f o r u s e r t o r e m o v e t h e m f r o m A l t S t o r e .
actions . append ( removeAction )
}
2020-05-16 15:34:10 -07:00
#endif
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
return actions
}
2023-03-01 00:48:36 -05:00
override func collectionView ( _ : UICollectionView , contextMenuConfigurationForItemAt indexPath : IndexPath , point _ : CGPoint ) -> UIContextMenuConfiguration ?
2020-03-11 14:43:19 -07:00
{
let section = Section ( rawValue : indexPath . section ) !
2023-03-01 00:48:36 -05:00
switch section {
2020-03-11 14:43:19 -07:00
case . updates , . noUpdates : return nil
case . activeApps , . inactiveApps :
2023-03-01 00:48:36 -05:00
let installedApp = dataSource . item ( at : indexPath )
return UIContextMenuConfiguration ( identifier : indexPath as NSIndexPath , previewProvider : nil ) { _ -> UIMenu ? in
2020-03-11 14:43:19 -07:00
let actions = self . actions ( for : installedApp )
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
let menu = UIMenu ( title : " " , children : actions )
return menu
}
}
}
2023-03-01 00:48:36 -05:00
override func collectionView ( _ collectionView : UICollectionView , previewForHighlightingContextMenuWithConfiguration configuration : UIContextMenuConfiguration ) -> UITargetedPreview ? {
2020-03-11 14:43:19 -07:00
guard let indexPath = configuration . identifier as ? NSIndexPath else { return nil }
guard let cell = collectionView . cellForItem ( at : indexPath as IndexPath ) as ? InstalledAppCollectionViewCell else { return nil }
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
let parameters = UIPreviewParameters ( )
parameters . backgroundColor = . clear
parameters . visiblePath = UIBezierPath ( roundedRect : cell . bannerView . bounds , cornerRadius : cell . bannerView . layer . cornerRadius )
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
let preview = UITargetedPreview ( view : cell . bannerView , parameters : parameters )
return preview
}
2023-03-01 00:48:36 -05:00
override func collectionView ( _ collectionView : UICollectionView , previewForDismissingContextMenuWithConfiguration configuration : UIContextMenuConfiguration ) -> UITargetedPreview ? {
self . collectionView ( collectionView , previewForHighlightingContextMenuWithConfiguration : configuration )
2020-03-11 14:43:19 -07:00
}
}
2023-03-01 00:48:36 -05:00
extension MyAppsViewController : UICollectionViewDelegateFlowLayout {
func collectionView ( _ collectionView : UICollectionView , layout _ : UICollectionViewLayout , sizeForItemAt indexPath : IndexPath ) -> CGSize {
2019-07-19 16:42:40 -07:00
let section = Section . allCases [ indexPath . section ]
2023-03-01 00:48:36 -05:00
switch section {
2019-07-24 15:01:33 -07:00
case . noUpdates :
2019-10-23 13:32:17 -07:00
let size = CGSize ( width : collectionView . bounds . width , height : 44 )
2019-07-24 15:01:33 -07:00
return size
2023-03-01 00:48:36 -05:00
2019-07-19 16:42:40 -07:00
case . updates :
2023-03-01 00:48:36 -05:00
let item = dataSource . item ( at : indexPath )
if let previousHeight = cachedUpdateSizes [ item . bundleIdentifier ] {
2019-07-19 16:42:40 -07:00
return previousHeight
}
2023-03-01 00:48:36 -05:00
2019-10-23 13:32:17 -07:00
// M a n u a l l y c h a n g e c e l l ' s w i d t h t o p r e v e n t c o n f l i c t i n g w i t h U I V i e w - E n c a p s u l a t e d - L a y o u t - W i d t h c o n s t r a i n t s .
2023-03-01 00:48:36 -05:00
prototypeUpdateCell . frame . size . width = collectionView . bounds . width
let widthConstraint = prototypeUpdateCell . contentView . widthAnchor . constraint ( equalToConstant : collectionView . bounds . width )
2019-07-19 16:42:40 -07:00
NSLayoutConstraint . activate ( [ widthConstraint ] )
defer { NSLayoutConstraint . deactivate ( [ widthConstraint ] ) }
2023-03-01 00:48:36 -05:00
dataSource . cellConfigurationHandler ( prototypeUpdateCell , item , indexPath )
let size = prototypeUpdateCell . contentView . systemLayoutSizeFitting ( UIView . layoutFittingCompressedSize )
cachedUpdateSizes [ item . bundleIdentifier ] = size
2019-07-19 16:42:40 -07:00
return size
2020-03-11 14:43:19 -07:00
case . activeApps , . inactiveApps :
2019-10-23 13:32:17 -07:00
return CGSize ( width : collectionView . bounds . width , height : 88 )
2019-07-19 16:42:40 -07:00
}
}
2023-03-01 00:48:36 -05:00
func collectionView ( _ collectionView : UICollectionView , layout _ : UICollectionViewLayout , referenceSizeForHeaderInSection section : Int ) -> CGSize {
2019-07-19 16:42:40 -07:00
let section = Section . allCases [ section ]
2023-03-01 00:48:36 -05:00
switch section {
2019-07-24 15:01:33 -07:00
case . noUpdates : return . zero
case . updates :
2023-03-01 00:48:36 -05:00
let height : CGFloat = updatesDataSource . itemCount > maximumCollapsedUpdatesCount ? 26 : 0
2019-07-24 15:01:33 -07:00
return CGSize ( width : collectionView . bounds . width , height : height )
2023-03-01 00:48:36 -05:00
2020-03-11 14:43:19 -07:00
case . activeApps : return CGSize ( width : collectionView . bounds . width , height : 29 )
2023-03-01 00:48:36 -05:00
case . inactiveApps where inactiveAppsDataSource . itemCount = = 0 : return . zero
2020-03-11 14:43:19 -07:00
case . inactiveApps : return CGSize ( width : collectionView . bounds . width , height : 29 )
2019-07-19 16:42:40 -07:00
}
}
2023-03-01 00:48:36 -05:00
func collectionView ( _ collectionView : UICollectionView , layout _ : UICollectionViewLayout , referenceSizeForFooterInSection section : Int ) -> CGSize {
2020-01-24 14:54:52 -08:00
let section = Section . allCases [ section ]
2023-03-01 00:48:36 -05:00
func appIDsFooterSize ( ) -> CGSize {
2020-02-10 17:30:11 -08:00
guard let _ = DatabaseManager . shared . activeTeam ( ) else { return . zero }
2023-03-01 00:48:36 -05:00
2020-02-10 17:30:11 -08:00
let indexPath = IndexPath ( row : 0 , section : section . rawValue )
let footerView = self . collectionView ( collectionView , viewForSupplementaryElementOfKind : UICollectionView . elementKindSectionFooter , at : indexPath ) as ! InstalledAppsCollectionFooterView
2023-03-01 00:48:36 -05:00
2020-02-10 17:30:11 -08:00
let size = footerView . systemLayoutSizeFitting ( CGSize ( width : collectionView . frame . width , height : UIView . layoutFittingExpandedSize . height ) ,
withHorizontalFittingPriority : . required ,
verticalFittingPriority : . fittingSizeLevel )
return size
2020-01-24 14:54:52 -08:00
}
2023-03-01 00:48:36 -05:00
switch section {
2020-03-11 14:43:19 -07:00
case . noUpdates : return . zero
case . updates : return . zero
2023-03-01 00:48:36 -05:00
case . activeApps where inactiveAppsDataSource . itemCount = = 0 : return appIDsFooterSize ( )
2020-03-11 14:43:19 -07:00
case . activeApps : return . zero
2023-03-01 00:48:36 -05:00
case . inactiveApps where inactiveAppsDataSource . itemCount = = 0 : return . zero
2020-03-11 14:43:19 -07:00
case . inactiveApps : return appIDsFooterSize ( )
}
2020-01-24 14:54:52 -08:00
}
2023-03-01 00:48:36 -05:00
func collectionView ( _ : UICollectionView , layout _ : UICollectionViewLayout , insetForSectionAt section : Int ) -> UIEdgeInsets {
2019-07-19 16:42:40 -07:00
let section = Section . allCases [ section ]
2023-03-01 00:48:36 -05:00
switch section {
case . noUpdates where updatesDataSource . itemCount != 0 : return . zero
case . updates where updatesDataSource . itemCount = = 0 : return . zero
2019-10-23 13:32:17 -07:00
default : return UIEdgeInsets ( top : 12 , left : 0 , bottom : 20 , right : 0 )
2019-07-24 15:01:33 -07:00
}
}
}
2023-03-01 00:48:36 -05:00
extension MyAppsViewController : UICollectionViewDragDelegate {
func collectionView ( _ collectionView : UICollectionView , itemsForBeginning _ : UIDragSession , at indexPath : IndexPath ) -> [ UIDragItem ] {
switch Section ( rawValue : indexPath . section ) ! {
2020-03-20 16:32:31 -07:00
case . updates , . noUpdates :
return [ ]
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
case . activeApps , . inactiveApps :
guard UserDefaults . standard . activeAppsLimit != nil else { return [ ] }
guard let cell = collectionView . cellForItem ( at : indexPath as IndexPath ) as ? InstalledAppCollectionViewCell else { return [ ] }
2023-03-01 00:48:36 -05:00
let item = dataSource . item ( at : indexPath )
2020-03-20 16:32:31 -07:00
guard item . bundleIdentifier != StoreApp . altstoreAppID else { return [ ] }
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
let dragItem = UIDragItem ( itemProvider : NSItemProvider ( item : nil , typeIdentifier : nil ) )
dragItem . localObject = item
dragItem . previewProvider = {
let parameters = UIDragPreviewParameters ( )
parameters . backgroundColor = . clear
parameters . visiblePath = UIBezierPath ( roundedRect : cell . bannerView . iconImageView . bounds , cornerRadius : cell . bannerView . iconImageView . layer . cornerRadius )
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
let preview = UIDragPreview ( view : cell . bannerView . iconImageView , parameters : parameters )
return preview
}
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
return [ dragItem ]
}
}
2023-03-01 00:48:36 -05:00
func collectionView ( _ collectionView : UICollectionView , dragPreviewParametersForItemAt indexPath : IndexPath ) -> UIDragPreviewParameters ? {
2020-03-20 16:32:31 -07:00
guard let cell = collectionView . cellForItem ( at : indexPath as IndexPath ) as ? InstalledAppCollectionViewCell else { return nil }
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
let parameters = UIDragPreviewParameters ( )
parameters . backgroundColor = . clear
parameters . visiblePath = UIBezierPath ( roundedRect : cell . bannerView . frame , cornerRadius : cell . bannerView . layer . cornerRadius )
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
return parameters
}
2023-03-01 00:48:36 -05:00
func collectionView ( _ : UICollectionView , dragSessionDidEnd _ : UIDragSession ) {
let previousDestinationIndexPath = dropDestinationIndexPath
dropDestinationIndexPath = nil
if let indexPath = previousDestinationIndexPath {
2020-03-20 16:32:31 -07:00
// A c c e s s c e l l d i r e c t l y t o p r e v e n t U I g l i t c h e s d u e t o r a c e c o n d i t i o n s w h e n r e f r e s h i n g
2023-03-01 00:48:36 -05:00
updateCell ( at : indexPath )
2020-03-20 16:32:31 -07:00
}
}
}
2023-03-01 00:48:36 -05:00
extension MyAppsViewController : UICollectionViewDropDelegate {
func collectionView ( _ : UICollectionView , canHandle session : UIDropSession ) -> Bool {
session . localDragSession != nil
2020-03-20 16:32:31 -07:00
}
2023-03-01 00:48:36 -05:00
func collectionView ( _ collectionView : UICollectionView , dropSessionDidUpdate session : UIDropSession , withDestinationIndexPath _ : IndexPath ? ) -> UICollectionViewDropProposal {
2020-03-20 16:32:31 -07:00
guard
let activeAppsLimit = UserDefaults . standard . activeAppsLimit ,
let installedApp = session . items . first ? . localObject as ? InstalledApp
else { return UICollectionViewDropProposal ( operation : . cancel ) }
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
// R e t r i e v e h e a d e r a t t r i b u t e s f o r l o c a t i o n c a l c u l a t i o n s .
guard
let activeAppsHeaderAttributes = collectionView . layoutAttributesForSupplementaryElement ( ofKind : UICollectionView . elementKindSectionHeader , at : IndexPath ( item : 0 , section : Section . activeApps . rawValue ) ) ,
let inactiveAppsHeaderAttributes = collectionView . layoutAttributesForSupplementaryElement ( ofKind : UICollectionView . elementKindSectionHeader , at : IndexPath ( item : 0 , section : Section . inactiveApps . rawValue ) )
else { return UICollectionViewDropProposal ( operation : . cancel ) }
2023-03-01 00:48:36 -05:00
var dropDestinationIndexPath : IndexPath ?
defer {
2020-03-20 16:32:31 -07:00
// A n i m a t e s e l e c t i o n c h a n g e s .
2023-03-01 00:48:36 -05:00
if dropDestinationIndexPath != self . dropDestinationIndexPath {
2020-03-20 16:32:31 -07:00
let previousIndexPath = self . dropDestinationIndexPath
self . dropDestinationIndexPath = dropDestinationIndexPath
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
let indexPaths = [ previousIndexPath , dropDestinationIndexPath ] . compactMap { $0 }
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
let propertyAnimator = UIViewPropertyAnimator ( springTimingParameters : UISpringTimingParameters ( ) ) {
2023-03-01 00:48:36 -05:00
for indexPath in indexPaths {
2020-03-20 16:32:31 -07:00
// A c c e s s c e l l d i r e c t l y s o w e c a n a n i m a t e i t c o r r e c t l y .
self . updateCell ( at : indexPath )
}
}
propertyAnimator . startAnimation ( )
}
}
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
let point = session . location ( in : collectionView )
2023-03-01 00:48:36 -05:00
if installedApp . isActive {
2020-03-20 16:32:31 -07:00
// D e a c t i v a t i n g
2023-03-01 00:48:36 -05:00
if point . y > inactiveAppsHeaderAttributes . frame . minY {
2020-03-20 16:32:31 -07:00
// I n a c t i v e a p p s s e c t i o n .
return UICollectionViewDropProposal ( operation : . copy , intent : . insertAtDestinationIndexPath )
2023-03-01 00:48:36 -05:00
} else if point . y > activeAppsHeaderAttributes . frame . minY {
2020-03-20 16:32:31 -07:00
// A c t i v e a p p s s e c t i o n .
return UICollectionViewDropProposal ( operation : . move , intent : . insertAtDestinationIndexPath )
2023-03-01 00:48:36 -05:00
} else {
2020-03-20 16:32:31 -07:00
return UICollectionViewDropProposal ( operation : . cancel )
}
2023-03-01 00:48:36 -05:00
} else {
2020-03-20 16:32:31 -07:00
// A c t i v a t i n g
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
guard point . y > activeAppsHeaderAttributes . frame . minY else {
// A b o v e a c t i v e a p p s s e c t i o n .
return UICollectionViewDropProposal ( operation : . cancel )
}
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
guard point . y < inactiveAppsHeaderAttributes . frame . minY else {
// I n a c t i v e a p p s s e c t i o n .
return UICollectionViewDropProposal ( operation : . move , intent : . insertAtDestinationIndexPath )
}
2023-03-01 00:48:36 -05:00
let activeAppsCount = ( activeAppsDataSource . fetchedResultsController . fetchedObjects ? ? [ ] ) . map { $0 . requiredActiveSlots } . reduce ( 0 , + )
2020-03-20 16:32:31 -07:00
let availableActiveApps = max ( activeAppsLimit - activeAppsCount , 0 )
2023-03-01 00:48:36 -05:00
if installedApp . requiredActiveSlots <= availableActiveApps {
2020-03-20 16:32:31 -07:00
// E n o u g h a c t i v e a p p s l o t s , s o n o n e e d t o d e a c t i v a t e a p p f i r s t .
return UICollectionViewDropProposal ( operation : . copy , intent : . insertAtDestinationIndexPath )
2023-03-01 00:48:36 -05:00
} else {
2020-03-20 16:32:31 -07:00
// N o t e n o u g h a c t i v e a p p s l o t s , s o w e n e e d t o d e a c t i v a t e a n a p p .
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
// P r o v i d e d d e s t i n a t i o n I n d e x P a t h i s i n a c c u r a t e .
guard let indexPath = collectionView . indexPathForItem ( at : point ) , indexPath . section = = Section . activeApps . rawValue else {
// I n v a l i d d e s t i n a t i o n i n d e x p a t h .
return UICollectionViewDropProposal ( operation : . cancel )
}
2023-03-01 00:48:36 -05:00
let installedApp = dataSource . item ( at : indexPath )
2020-03-20 16:32:31 -07:00
guard installedApp . bundleIdentifier != StoreApp . altstoreAppID else {
// C a n ' t d e a c t i v a t e A l t S t o r e .
return UICollectionViewDropProposal ( operation : . forbidden , intent : . insertIntoDestinationIndexPath )
}
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
// T h i s a p p c a n b e d e a c t i v a t e d !
dropDestinationIndexPath = indexPath
return UICollectionViewDropProposal ( operation : . move , intent : . insertIntoDestinationIndexPath )
}
}
}
2023-03-01 00:48:36 -05:00
func collectionView ( _ : UICollectionView , performDropWith coordinator : UICollectionViewDropCoordinator ) {
2020-03-20 16:32:31 -07:00
guard let installedApp = coordinator . session . items . first ? . localObject as ? InstalledApp else { return }
guard let destinationIndexPath = coordinator . destinationIndexPath else { return }
2023-03-01 00:48:36 -05:00
if installedApp . isActive {
2020-03-20 16:32:31 -07:00
guard destinationIndexPath . section = = Section . inactiveApps . rawValue else { return }
2023-03-01 00:48:36 -05:00
deactivate ( installedApp )
} else {
2020-03-20 16:32:31 -07:00
guard destinationIndexPath . section = = Section . activeApps . rawValue else { return }
2023-03-01 00:48:36 -05:00
switch coordinator . proposal . intent {
2020-03-20 16:32:31 -07:00
case . insertIntoDestinationIndexPath :
installedApp . isActive = true
2023-03-01 00:48:36 -05:00
let previousInstalledApp = dataSource . item ( at : destinationIndexPath )
deactivate ( previousInstalledApp ) { result in
2020-03-20 16:32:31 -07:00
installedApp . managedObjectContext ? . perform {
2023-03-01 00:48:36 -05:00
switch result {
2020-03-20 16:32:31 -07:00
case . failure : installedApp . isActive = false
case . success : self . activate ( installedApp )
}
}
}
2023-03-01 00:48:36 -05:00
2020-03-20 16:32:31 -07:00
case . insertAtDestinationIndexPath :
2023-03-01 00:48:36 -05:00
activate ( installedApp )
2020-03-20 16:32:31 -07:00
case . unspecified : break
@ unknown default : break
}
}
}
}
2023-03-01 00:48:36 -05:00
extension MyAppsViewController : NSFetchedResultsControllerDelegate {
func controllerWillChangeContent ( _ controller : NSFetchedResultsController < NSFetchRequestResult > ) {
2019-07-31 11:24:18 -07:00
// R e s p o n d i n g t o N S F e t c h e d R e s u l t s C o n t r o l l e r u p d a t e s b e f o r e t h e c o l l e c t i o n v i e w h a s
// b e e n s h o w n m a y t h r o w e x c e p t i o n s b e c a u s e t h e c o l l e c t i o n v i e w c a n n o t a c c u r a t e l y
// c o u n t t h e n u m b e r o f i t e m s b e f o r e t h e u p d a t e . H o w e v e r , i f w e m a n u a l l y c a l l
// p e r f o r m B a t c h U p d a t e s _ b e f o r e _ r e s p o n d i n g t o u p d a t e s , t h e c o l l e c t i o n v i e w c a n g e t
// a n a c c u r a t e p r e - u p d a t e i t e m c o u n t .
2023-03-01 00:48:36 -05:00
collectionView . performBatchUpdates ( nil , completion : nil )
updatesDataSource . controllerWillChangeContent ( controller )
2019-07-24 15:01:33 -07:00
}
2023-03-01 00:48:36 -05:00
func controller ( _ controller : NSFetchedResultsController < NSFetchRequestResult > , didChange sectionInfo : NSFetchedResultsSectionInfo , atSectionIndex sectionIndex : Int , for type : NSFetchedResultsChangeType ) {
updatesDataSource . controller ( controller , didChange : sectionInfo , atSectionIndex : UInt ( sectionIndex ) , for : type )
2019-07-24 15:01:33 -07:00
}
2023-03-01 00:48:36 -05:00
func controller ( _ controller : NSFetchedResultsController < NSFetchRequestResult > , didChange anObject : Any , at indexPath : IndexPath ? , for type : NSFetchedResultsChangeType , newIndexPath : IndexPath ? ) {
updatesDataSource . controller ( controller , didChange : anObject , at : indexPath , for : type , newIndexPath : newIndexPath )
2019-07-24 15:01:33 -07:00
}
2023-03-01 00:48:36 -05:00
func controllerDidChangeContent ( _ controller : NSFetchedResultsController < NSFetchRequestResult > ) {
let previousUpdateCount = collectionView . numberOfItems ( inSection : Section . updates . rawValue )
let updateCount = Int ( updatesDataSource . itemCount )
if previousUpdateCount = = 0 && updateCount > 0 {
2019-07-24 15:01:33 -07:00
// R e m o v e " N o U p d a t e s A v a i l a b l e " c e l l .
let change = RSTCellContentChange ( type : . delete , currentIndexPath : IndexPath ( item : 0 , section : Section . noUpdates . rawValue ) , destinationIndexPath : nil )
2023-03-01 00:48:36 -05:00
collectionView . add ( change )
} else if previousUpdateCount > 0 && updateCount = = 0 {
2019-07-24 15:01:33 -07:00
// I n s e r t " N o U p d a t e s A v a i l a b l e " c e l l .
let change = RSTCellContentChange ( type : . insert , currentIndexPath : nil , destinationIndexPath : IndexPath ( item : 0 , section : Section . noUpdates . rawValue ) )
2023-03-01 00:48:36 -05:00
collectionView . add ( change )
2019-07-24 15:01:33 -07:00
}
2023-03-01 00:48:36 -05:00
updatesDataSource . controllerDidChangeContent ( controller )
2019-06-06 12:56:13 -07:00
}
2019-05-20 21:36:39 +02:00
}
2019-07-28 15:51:36 -07:00
2023-03-01 00:48:36 -05:00
extension MyAppsViewController : UIDocumentPickerDelegate {
func documentPicker ( _ controller : UIDocumentPickerViewController , didPickDocumentsAt urls : [ URL ] ) {
2019-07-28 15:51:36 -07:00
guard let fileURL = urls . first else { return }
2023-03-01 00:48:36 -05:00
switch controller . documentPickerMode {
2020-05-16 16:34:50 -07:00
case . import , . open :
2023-03-01 00:48:36 -05:00
sideloadApp ( at : fileURL ) { result in
2023-03-02 00:40:11 -05:00
os_log ( " Sideloaded app at %@ with result: %@ " , type : . info , fileURL . absoluteString , String ( describing : result ) )
2020-05-16 16:34:50 -07:00
}
2023-03-01 00:48:36 -05:00
2020-05-16 16:34:50 -07:00
case . exportToService , . moveToService : break
@ unknown default : break
2019-07-28 15:51:36 -07:00
}
}
}
2019-09-12 13:51:03 -07:00
2023-03-01 00:48:36 -05:00
extension MyAppsViewController : UIViewControllerPreviewingDelegate {
func previewingContext ( _ previewingContext : UIViewControllerPreviewing , viewControllerForLocation location : CGPoint ) -> UIViewController ? {
2019-09-12 13:51:03 -07:00
guard
2023-03-01 00:48:36 -05:00
let indexPath = collectionView . indexPathForItem ( at : location ) ,
let cell = collectionView . cellForItem ( at : indexPath )
2019-09-12 13:51:03 -07:00
else { return nil }
2023-03-01 00:48:36 -05:00
2019-09-12 13:51:03 -07:00
let section = Section . allCases [ indexPath . section ]
2023-03-01 00:48:36 -05:00
switch section {
2019-09-12 13:51:03 -07:00
case . updates :
previewingContext . sourceRect = cell . frame
2023-03-01 00:48:36 -05:00
let app = dataSource . item ( at : indexPath )
guard let storeApp = app . storeApp else { return nil }
2019-09-12 13:51:03 -07:00
let appViewController = AppViewController . makeAppViewController ( app : storeApp )
return appViewController
2023-03-01 00:48:36 -05:00
2019-09-12 13:51:03 -07:00
default : return nil
}
}
2023-03-01 00:48:36 -05:00
func previewingContext ( _ previewingContext : UIViewControllerPreviewing , commit _ : UIViewController ) {
2019-09-12 13:51:03 -07:00
let point = CGPoint ( x : previewingContext . sourceRect . midX , y : previewingContext . sourceRect . midY )
2023-03-01 00:48:36 -05:00
guard let indexPath = collectionView . indexPathForItem ( at : point ) , let cell = collectionView . cellForItem ( at : indexPath ) else { return }
performSegue ( withIdentifier : " showUpdate " , sender : cell )
2019-09-12 13:51:03 -07:00
}
}
2020-10-01 14:09:45 -07:00
2023-03-01 00:48:36 -05:00
extension MyAppsViewController : UIImagePickerControllerDelegate , UINavigationControllerDelegate {
func imagePickerController ( _ picker : UIImagePickerController , didFinishPickingMediaWithInfo info : [ UIImagePickerController . InfoKey : Any ] ) {
2020-10-01 14:09:45 -07:00
defer {
picker . dismiss ( animated : true , completion : nil )
self . _imagePickerInstalledApp = nil
}
2023-03-01 00:48:36 -05:00
guard let image = info [ . editedImage ] as ? UIImage , let installedApp = _imagePickerInstalledApp else { return }
changeIcon ( for : installedApp , to : image )
2020-10-01 14:09:45 -07:00
}
2023-03-01 00:48:36 -05:00
func imagePickerControllerDidCancel ( _ picker : UIImagePickerController ) {
2020-10-01 14:09:45 -07:00
picker . dismiss ( animated : true , completion : nil )
2023-03-01 00:48:36 -05:00
_imagePickerInstalledApp = nil
2020-10-01 14:09:45 -07:00
}
}