mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-13 00:33:28 +01:00
Adds support for activating and deactivating apps
iOS 13.3.1 limits free developer accounts to 3 apps and app extensions. As a workaround, we now allow up to 3 “active” apps (apps with installed provisioning profiles), as well as additional “inactivate” apps which don’t have any profiles installed, causing them to not count towards the total. Inactive apps cannot be opened until they are activated.
This commit is contained in:
@@ -162,6 +162,9 @@
|
|||||||
BFBBE2DF22931F73002097FA /* StoreApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* StoreApp.swift */; };
|
BFBBE2DF22931F73002097FA /* StoreApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* StoreApp.swift */; };
|
||||||
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2E022931F81002097FA /* InstalledApp.swift */; };
|
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2E022931F81002097FA /* InstalledApp.swift */; };
|
||||||
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */; };
|
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */; };
|
||||||
|
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */; };
|
||||||
|
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */; };
|
||||||
|
BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFC57A6F2416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib */; };
|
||||||
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476D2284B9A500981D42 /* AppDelegate.swift */; };
|
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476D2284B9A500981D42 /* AppDelegate.swift */; };
|
||||||
BFD247752284B9A500981D42 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247732284B9A500981D42 /* Main.storyboard */; };
|
BFD247752284B9A500981D42 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247732284B9A500981D42 /* Main.storyboard */; };
|
||||||
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD247762284B9A700981D42 /* Assets.xcassets */; };
|
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD247762284B9A700981D42 /* Assets.xcassets */; };
|
||||||
@@ -484,6 +487,9 @@
|
|||||||
BFBBE2DE22931F73002097FA /* StoreApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp.swift; sourceTree = "<group>"; };
|
BFBBE2DE22931F73002097FA /* StoreApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp.swift; sourceTree = "<group>"; };
|
||||||
BFBBE2E022931F81002097FA /* InstalledApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledApp.swift; sourceTree = "<group>"; };
|
BFBBE2E022931F81002097FA /* InstalledApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledApp.swift; sourceTree = "<group>"; };
|
||||||
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadAppOperation.swift; sourceTree = "<group>"; };
|
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadAppOperation.swift; sourceTree = "<group>"; };
|
||||||
|
BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeactivateAppOperation.swift; sourceTree = "<group>"; };
|
||||||
|
BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledAppsCollectionHeaderView.swift; sourceTree = "<group>"; };
|
||||||
|
BFC57A6F2416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstalledAppsCollectionHeaderView.xib; sourceTree = "<group>"; };
|
||||||
BFD2476A2284B9A500981D42 /* AltStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltStore.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
BFD2476A2284B9A500981D42 /* AltStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltStore.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
BFD2476D2284B9A500981D42 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
BFD2476D2284B9A500981D42 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
BFD247742284B9A500981D42 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
BFD247742284B9A500981D42 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
@@ -936,6 +942,8 @@
|
|||||||
BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */,
|
BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */,
|
||||||
BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */,
|
BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */,
|
||||||
BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */,
|
BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */,
|
||||||
|
BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */,
|
||||||
|
BFC57A6F2416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib */,
|
||||||
);
|
);
|
||||||
path = "My Apps";
|
path = "My Apps";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1176,6 +1184,7 @@
|
|||||||
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */,
|
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */,
|
||||||
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */,
|
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */,
|
||||||
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
|
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
|
||||||
|
BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */,
|
||||||
);
|
);
|
||||||
path = Operations;
|
path = Operations;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1428,6 +1437,7 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */,
|
||||||
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */,
|
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */,
|
||||||
BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */,
|
BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */,
|
||||||
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
|
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
|
||||||
@@ -1684,6 +1694,7 @@
|
|||||||
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
||||||
BFBBE2DF22931F73002097FA /* StoreApp.swift in Sources */,
|
BFBBE2DF22931F73002097FA /* StoreApp.swift in Sources */,
|
||||||
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
|
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
|
||||||
|
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */,
|
||||||
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
|
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
|
||||||
BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */,
|
BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */,
|
||||||
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */,
|
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */,
|
||||||
@@ -1709,6 +1720,7 @@
|
|||||||
BF100C54232D7DAE006A8926 /* StoreAppPolicy.swift in Sources */,
|
BF100C54232D7DAE006A8926 /* StoreAppPolicy.swift in Sources */,
|
||||||
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
|
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
|
||||||
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
|
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
|
||||||
|
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */,
|
||||||
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
|
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
|
||||||
BF56D2AA23DF88310006506D /* AppID.swift in Sources */,
|
BF56D2AA23DF88310006506D /* AppID.swift in Sources */,
|
||||||
BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */,
|
BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="wKh-xq-NuP">
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
|
||||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8Tg-wk-r0u">
|
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8Tg-wk-r0u">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" insetsLayoutMarginsFromSafeArea="NO" id="oNk-OQ-r4M">
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="oNk-OQ-r4M">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
</view>
|
</view>
|
||||||
@@ -133,14 +133,14 @@
|
|||||||
<visualEffectView opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tUK-0J-07U">
|
<visualEffectView opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tUK-0J-07U">
|
||||||
<rect key="frame" x="58" y="117" width="18" height="18"/>
|
<rect key="frame" x="58" y="117" width="18" height="18"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" insetsLayoutMarginsFromSafeArea="NO" id="yyn-wP-xk4">
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="yyn-wP-xk4">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
|
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="JP7-6F-CoG">
|
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="JP7-6F-CoG">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
|
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" insetsLayoutMarginsFromSafeArea="NO" id="UJ5-ia-PVA">
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="UJ5-ia-PVA">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
|
<rect key="frame" x="0.0" y="0.0" width="18" height="18"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
@@ -628,13 +628,13 @@ World</string>
|
|||||||
<color key="backgroundColor" name="Background"/>
|
<color key="backgroundColor" name="Background"/>
|
||||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="SB5-U0-jyy">
|
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="SB5-U0-jyy">
|
||||||
<size key="itemSize" width="375" height="60"/>
|
<size key="itemSize" width="375" height="60"/>
|
||||||
<size key="headerReferenceSize" width="50" height="50"/>
|
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||||
<size key="footerReferenceSize" width="50" height="60.5"/>
|
<size key="footerReferenceSize" width="50" height="60.5"/>
|
||||||
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
|
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||||
</collectionViewFlowLayout>
|
</collectionViewFlowLayout>
|
||||||
<cells>
|
<cells>
|
||||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="AppCell" id="kMp-ym-2yu" customClass="InstalledAppCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="AppCell" id="kMp-ym-2yu" customClass="InstalledAppCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="50" width="375" height="60"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||||
@@ -663,7 +663,7 @@ World</string>
|
|||||||
</connections>
|
</connections>
|
||||||
</collectionViewCell>
|
</collectionViewCell>
|
||||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="NoUpdatesCell" id="h0f-XI-UA5" customClass="NoUpdatesCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="NoUpdatesCell" id="h0f-XI-UA5" customClass="NoUpdatesCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="125" width="375" height="60"/>
|
<rect key="frame" x="0.0" y="75" width="375" height="60"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||||
@@ -723,35 +723,8 @@ World</string>
|
|||||||
</connections>
|
</connections>
|
||||||
</collectionViewCell>
|
</collectionViewCell>
|
||||||
</cells>
|
</cells>
|
||||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="InstalledAppsHeader" id="Crb-NU-1Ye" customClass="InstalledAppsCollectionHeaderView" customModule="AltStore" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="50"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<subviews>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Installed" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BDU-hM-rro">
|
|
||||||
<rect key="frame" x="20" y="21" width="96.5" height="29"/>
|
|
||||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="24"/>
|
|
||||||
<nil key="textColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="nxk-e8-ARx">
|
|
||||||
<rect key="frame" x="274" y="23" width="81" height="32"/>
|
|
||||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="16"/>
|
|
||||||
<state key="normal" title="Refresh All"/>
|
|
||||||
</button>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="BDU-hM-rro" secondAttribute="bottom" id="9iT-ur-A4W"/>
|
|
||||||
<constraint firstItem="BDU-hM-rro" firstAttribute="leading" secondItem="Crb-NU-1Ye" secondAttribute="leading" constant="20" id="F8e-9W-MC2"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="nxk-e8-ARx" secondAttribute="trailing" constant="20" id="WxV-85-RcK"/>
|
|
||||||
<constraint firstItem="nxk-e8-ARx" firstAttribute="firstBaseline" secondItem="BDU-hM-rro" secondAttribute="firstBaseline" id="lIO-3C-ZPH"/>
|
|
||||||
</constraints>
|
|
||||||
<connections>
|
|
||||||
<outlet property="button" destination="nxk-e8-ARx" id="gwj-97-LVi"/>
|
|
||||||
<outlet property="textLabel" destination="BDU-hM-rro" id="CQM-8K-bcH"/>
|
|
||||||
</connections>
|
|
||||||
</collectionReusableView>
|
|
||||||
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="InstalledAppsFooter" id="HYs-co-nJZ" customClass="InstalledAppsCollectionFooterView" customModule="AltStore" customModuleProvider="target">
|
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="InstalledAppsFooter" id="HYs-co-nJZ" customClass="InstalledAppsCollectionFooterView" customModule="AltStore" customModuleProvider="target">
|
||||||
<rect key="frame" x="0.0" y="185" width="375" height="60.5"/>
|
<rect key="frame" x="0.0" y="135" width="375" height="60.5"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="900" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="GFQ-Wy-Qhy">
|
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="900" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="GFQ-Wy-Qhy">
|
||||||
@@ -951,6 +924,10 @@ World</string>
|
|||||||
<point key="canvasLocation" x="2526" y="731"/>
|
<point key="canvasLocation" x="2526" y="731"/>
|
||||||
</scene>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
|
<inferredMetricsTieBreakers>
|
||||||
|
<segue reference="cnd-KK-o60"/>
|
||||||
|
</inferredMetricsTieBreakers>
|
||||||
|
<color key="tintColor" name="Primary"/>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="Back" width="18" height="18"/>
|
<image name="Back" width="18" height="18"/>
|
||||||
<image name="Browse" width="20" height="20"/>
|
<image name="Browse" width="20" height="20"/>
|
||||||
@@ -967,8 +944,4 @@ World</string>
|
|||||||
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
<color red="0.0039215686274509803" green="0.50196078431372548" blue="0.51764705882352946" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</namedColor>
|
</namedColor>
|
||||||
</resources>
|
</resources>
|
||||||
<inferredMetricsTieBreakers>
|
|
||||||
<segue reference="cnd-KK-o60"/>
|
|
||||||
</inferredMetricsTieBreakers>
|
|
||||||
<color key="tintColor" name="Primary"/>
|
|
||||||
</document>
|
</document>
|
||||||
|
|||||||
@@ -22,6 +22,23 @@ extension UserDefaults
|
|||||||
|
|
||||||
@NSManaged var legacySideloadedApps: [String]?
|
@NSManaged var legacySideloadedApps: [String]?
|
||||||
|
|
||||||
|
var activeAppsLimit: Int? {
|
||||||
|
get {
|
||||||
|
return self._activeAppsLimit?.intValue
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if let value = newValue
|
||||||
|
{
|
||||||
|
self._activeAppsLimit = NSNumber(value: value)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self._activeAppsLimit = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@NSManaged @objc(activeAppsLimit) private var _activeAppsLimit: NSNumber?
|
||||||
|
|
||||||
func registerDefaults()
|
func registerDefaults()
|
||||||
{
|
{
|
||||||
self.register(defaults: [#keyPath(UserDefaults.isBackgroundRefreshEnabled): true])
|
self.register(defaults: [#keyPath(UserDefaults.isBackgroundRefreshEnabled): true])
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ extension AppManager
|
|||||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||||
fetchRequest.returnsObjectsAsFaults = false
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
|
||||||
|
var activeAppsCount = 0
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let installedApps = try context.fetch(fetchRequest)
|
let installedApps = try context.fetch(fetchRequest)
|
||||||
@@ -75,19 +77,29 @@ extension AppManager
|
|||||||
|
|
||||||
for app in installedApps
|
for app in installedApps
|
||||||
{
|
{
|
||||||
let uti = UTTypeCopyDeclaration(app.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
|
guard app.bundleIdentifier != StoreApp.altstoreAppID else {
|
||||||
|
|
||||||
if app.bundleIdentifier == StoreApp.altstoreAppID
|
|
||||||
{
|
|
||||||
self.scheduleExpirationWarningLocalNotification(for: app)
|
self.scheduleExpirationWarningLocalNotification(for: app)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
let uti = UTTypeCopyDeclaration(app.installedAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
|
||||||
|
guard uti != nil || legacySideloadedApps.contains(app.bundleIdentifier) else {
|
||||||
|
// This UTI is not declared by any apps, which means this app has been deleted by the user.
|
||||||
|
// This app is also not a legacy sideloaded app, so we can assume it's fine to delete it.
|
||||||
|
context.delete(app)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.isActive
|
||||||
{
|
{
|
||||||
if uti == nil && !legacySideloadedApps.contains(app.bundleIdentifier)
|
if let activeAppsLimit = UserDefaults.standard.activeAppsLimit, activeAppsCount >= activeAppsLimit - 1
|
||||||
{
|
{
|
||||||
// This UTI is not declared by any apps, which means this app has been deleted by the user.
|
// We have reached active apps limit (excluding AltStore itself), so mark additional active apps as inactive.
|
||||||
// This app is also not a legacy sideloaded app, so we can assume it's fine to delete it.
|
app.isActive = false
|
||||||
context.delete(app)
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
activeAppsCount += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,6 +227,42 @@ extension AppManager
|
|||||||
return self.perform(operations, presentingViewController: presentingViewController, group: group)
|
return self.perform(operations, presentingViewController: presentingViewController, group: group)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func activate(_ installedApp: InstalledApp, presentingViewController: UIViewController?, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let group = self.refresh([installedApp], presentingViewController: presentingViewController)
|
||||||
|
group.completionHandler = { (results) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
guard let result = results.values.first else { throw OperationError.unknown }
|
||||||
|
|
||||||
|
let installedApp = try result.get()
|
||||||
|
installedApp.managedObjectContext?.perform {
|
||||||
|
installedApp.isActive = true
|
||||||
|
completionHandler(.success(installedApp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deactivate(_ installedApp: InstalledApp, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void)
|
||||||
|
{
|
||||||
|
let context = OperationContext()
|
||||||
|
|
||||||
|
let findServerOperation = self.findServer(context: context) { _ in }
|
||||||
|
|
||||||
|
let deactivateAppOperation = DeactivateAppOperation(app: installedApp, context: context)
|
||||||
|
deactivateAppOperation.resultHandler = { (result) in
|
||||||
|
completionHandler(result)
|
||||||
|
}
|
||||||
|
deactivateAppOperation.addDependency(findServerOperation)
|
||||||
|
|
||||||
|
self.run([deactivateAppOperation], requiresSerialQueue: true)
|
||||||
|
}
|
||||||
|
|
||||||
func installationProgress(for app: AppProtocol) -> Progress?
|
func installationProgress(for app: AppProtocol) -> Progress?
|
||||||
{
|
{
|
||||||
let progress = self.installationProgress[app.bundleIdentifier]
|
let progress = self.installationProgress[app.bundleIdentifier]
|
||||||
@@ -468,7 +516,23 @@ private extension AppManager
|
|||||||
/* Refresh */
|
/* Refresh */
|
||||||
let refreshAppOperation = RefreshAppOperation(context: context)
|
let refreshAppOperation = RefreshAppOperation(context: context)
|
||||||
refreshAppOperation.resultHandler = { (result) in
|
refreshAppOperation.resultHandler = { (result) in
|
||||||
completionHandler(result)
|
switch result
|
||||||
|
{
|
||||||
|
case .success(let installedApp):
|
||||||
|
completionHandler(.success(installedApp))
|
||||||
|
|
||||||
|
case .failure(ALTServerError.unknownRequest):
|
||||||
|
// Fall back to installation if AltServer doesn't support newer provisioning profile requests.
|
||||||
|
app.managedObjectContext?.perform {
|
||||||
|
let installProgress = self._install(app, group: group) { (result) in
|
||||||
|
completionHandler(result)
|
||||||
|
}
|
||||||
|
progress.addChild(installProgress, withPendingUnitCount: 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
completionHandler(.failure(error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
progress.addChild(refreshAppOperation.progress, withPendingUnitCount: 40)
|
progress.addChild(refreshAppOperation.progress, withPendingUnitCount: 40)
|
||||||
refreshAppOperation.addDependency(fetchProvisioningProfilesOperation)
|
refreshAppOperation.addDependency(fetchProvisioningProfilesOperation)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
|
||||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="installedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="isActive" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
<attribute name="name" attributeType="String"/>
|
<attribute name="name" attributeType="String"/>
|
||||||
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="refreshedDate" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
<attribute name="resignedBundleIdentifier" attributeType="String"/>
|
||||||
@@ -154,8 +155,9 @@
|
|||||||
</entity>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||||
|
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
|
||||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="193"/>
|
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="223"/>
|
||||||
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
||||||
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
|
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
|
||||||
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
@@ -163,6 +165,5 @@
|
|||||||
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
|
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
|
||||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
||||||
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
|
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
|
||||||
<element name="AppID" positionX="-27" positionY="153" width="128" height="133"/>
|
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
||||||
@@ -194,9 +194,22 @@ private extension DatabaseManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cachedRefreshedDate = installedApp.refreshedDate
|
||||||
|
let cachedExpirationDate = installedApp.expirationDate
|
||||||
|
|
||||||
// Must go after comparing versions to see if we need to update our cached AltStore app bundle.
|
// Must go after comparing versions to see if we need to update our cached AltStore app bundle.
|
||||||
installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber)
|
installedApp.update(resignedApp: localApp, certificateSerialNumber: serialNumber)
|
||||||
|
|
||||||
|
if installedApp.refreshedDate < cachedRefreshedDate
|
||||||
|
{
|
||||||
|
// Embedded provisioning profile has a creation date older than our refreshed date.
|
||||||
|
// This most likely means we've refreshed the app since then, and profile is now outdated,
|
||||||
|
// so use cached dates instead (i.e. not the dates updated from provisioning profile).
|
||||||
|
|
||||||
|
installedApp.refreshedDate = cachedRefreshedDate
|
||||||
|
installedApp.expirationDate = cachedExpirationDate
|
||||||
|
}
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
try context.save()
|
try context.save()
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class InstalledApp: NSManagedObject, InstalledAppProtocol
|
|||||||
@NSManaged var expirationDate: Date
|
@NSManaged var expirationDate: Date
|
||||||
@NSManaged var installedDate: Date
|
@NSManaged var installedDate: Date
|
||||||
|
|
||||||
|
@NSManaged var isActive: Bool
|
||||||
|
|
||||||
@NSManaged var certificateSerialNumber: String?
|
@NSManaged var certificateSerialNumber: String?
|
||||||
|
|
||||||
/* Relationships */
|
/* Relationships */
|
||||||
@@ -98,7 +100,15 @@ extension InstalledApp
|
|||||||
class func updatesFetchRequest() -> NSFetchRequest<InstalledApp>
|
class func updatesFetchRequest() -> NSFetchRequest<InstalledApp>
|
||||||
{
|
{
|
||||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||||
fetchRequest.predicate = NSPredicate(format: "%K != nil AND %K != %K", #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.version))
|
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K != nil AND %K != %K",
|
||||||
|
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.version))
|
||||||
|
return fetchRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
class func activeAppsFetchRequest() -> NSFetchRequest<InstalledApp>
|
||||||
|
{
|
||||||
|
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
||||||
|
fetchRequest.predicate = NSPredicate(format: "%K == YES", #keyPath(InstalledApp.isActive))
|
||||||
return fetchRequest
|
return fetchRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,9 +120,15 @@ extension InstalledApp
|
|||||||
return altStore
|
return altStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class func fetchActiveApps(in context: NSManagedObjectContext) -> [InstalledApp]
|
||||||
|
{
|
||||||
|
let activeApps = InstalledApp.fetch(InstalledApp.activeAppsFetchRequest(), in: context)
|
||||||
|
return activeApps
|
||||||
|
}
|
||||||
|
|
||||||
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
|
class func fetchAppsForRefreshingAll(in context: NSManagedObjectContext) -> [InstalledApp]
|
||||||
{
|
{
|
||||||
var predicate = NSPredicate(format: "%K != %@", #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
var predicate = NSPredicate(format: "%K == YES AND %K != %@", #keyPath(InstalledApp.isActive), #keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||||
|
|
||||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
if let patreonAccount = DatabaseManager.shared.patreonAccount(in: context), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||||
{
|
{
|
||||||
@@ -142,7 +158,8 @@ extension InstalledApp
|
|||||||
// Date 6 hours before now.
|
// Date 6 hours before now.
|
||||||
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
|
let date = Date().addingTimeInterval(-1 * 6 * 60 * 60)
|
||||||
|
|
||||||
var predicate = NSPredicate(format: "(%K < %@) AND (%K != %@)",
|
var predicate = NSPredicate(format: "(%K == YES) AND (%K < %@) AND (%K != %@)",
|
||||||
|
#keyPath(InstalledApp.isActive),
|
||||||
#keyPath(InstalledApp.refreshedDate), date as NSDate,
|
#keyPath(InstalledApp.refreshedDate), date as NSDate,
|
||||||
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
#keyPath(InstalledApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||||
|
|
||||||
|
|||||||
43
AltStore/My Apps/InstalledAppsCollectionHeaderView.swift
Normal file
43
AltStore/My Apps/InstalledAppsCollectionHeaderView.swift
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//
|
||||||
|
// InstalledAppsCollectionHeaderView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/9/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class InstalledAppsCollectionHeaderView: UICollectionReusableView
|
||||||
|
{
|
||||||
|
let textLabel: UILabel
|
||||||
|
let button: UIButton
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
self.textLabel = UILabel()
|
||||||
|
self.textLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
self.textLabel.font = UIFont.systemFont(ofSize: 24, weight: .bold)
|
||||||
|
|
||||||
|
self.button = UIButton(type: .system)
|
||||||
|
self.button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
self.button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.textLabel)
|
||||||
|
self.addSubview(self.button)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([self.textLabel.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor),
|
||||||
|
self.textLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor)])
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([self.button.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor),
|
||||||
|
self.button.firstBaselineAnchor.constraint(equalTo: self.textLabel.firstBaselineAnchor)])
|
||||||
|
|
||||||
|
self.preservesSuperviewLayoutMargins = true
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
43
AltStore/My Apps/InstalledAppsCollectionHeaderView.xib
Normal file
43
AltStore/My Apps/InstalledAppsCollectionHeaderView.xib
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16092.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16082.1"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionReusableView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="InstalledAppsHeader" id="eyV-eW-aLi" customClass="InstalledAppsCollectionHeaderView" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="50"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Installed" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zhW-Re-WNf">
|
||||||
|
<rect key="frame" x="20" y="21" width="96.5" height="29"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="24"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="1000" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="iMf-wr-wRV">
|
||||||
|
<rect key="frame" x="274" y="23" width="81" height="32"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="16"/>
|
||||||
|
<state key="normal" title="Refresh All"/>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="zhW-Re-WNf" firstAttribute="leading" secondItem="eyV-eW-aLi" secondAttribute="leading" constant="20" id="Fo0-fL-UpD"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="zhW-Re-WNf" secondAttribute="bottom" id="OWw-FY-KOh"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="iMf-wr-wRV" secondAttribute="trailing" constant="20" id="dJM-7c-k31"/>
|
||||||
|
<constraint firstItem="iMf-wr-wRV" firstAttribute="firstBaseline" secondItem="zhW-Re-WNf" secondAttribute="firstBaseline" id="iU7-F2-XDu"/>
|
||||||
|
</constraints>
|
||||||
|
<viewLayoutGuide key="safeArea" id="N3q-SZ-Vyv"/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="button" destination="iMf-wr-wRV" id="kWT-cc-BjS"/>
|
||||||
|
<outlet property="textLabel" destination="zhW-Re-WNf" id="UOg-4X-rWx"/>
|
||||||
|
</connections>
|
||||||
|
<point key="canvasLocation" x="19.565217391304348" y="30.803571428571427"/>
|
||||||
|
</collectionReusableView>
|
||||||
|
</objects>
|
||||||
|
</document>
|
||||||
@@ -18,18 +18,9 @@ class InstalledAppCollectionViewCell: UICollectionViewCell
|
|||||||
|
|
||||||
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
self.contentView.preservesSuperviewLayoutMargins = true
|
self.contentView.preservesSuperviewLayoutMargins = true
|
||||||
|
|
||||||
self.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
|
|
||||||
self.bannerView.buttonLabel.isHidden = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InstalledAppsCollectionHeaderView: UICollectionReusableView
|
|
||||||
{
|
|
||||||
@IBOutlet var textLabel: UILabel!
|
|
||||||
@IBOutlet var button: UIButton!
|
|
||||||
}
|
|
||||||
|
|
||||||
class InstalledAppsCollectionFooterView: UICollectionReusableView
|
class InstalledAppsCollectionFooterView: UICollectionReusableView
|
||||||
{
|
{
|
||||||
@IBOutlet var textLabel: UILabel!
|
@IBOutlet var textLabel: UILabel!
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
case noUpdates
|
case noUpdates
|
||||||
case updates
|
case updates
|
||||||
case installedApps
|
case activeApps
|
||||||
|
case inactiveApps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,10 +33,10 @@ class MyAppsViewController: UICollectionViewController
|
|||||||
private lazy var dataSource = self.makeDataSource()
|
private lazy var dataSource = self.makeDataSource()
|
||||||
private lazy var noUpdatesDataSource = self.makeNoUpdatesDataSource()
|
private lazy var noUpdatesDataSource = self.makeNoUpdatesDataSource()
|
||||||
private lazy var updatesDataSource = self.makeUpdatesDataSource()
|
private lazy var updatesDataSource = self.makeUpdatesDataSource()
|
||||||
private lazy var installedAppsDataSource = self.makeInstalledAppsDataSource()
|
private lazy var activeAppsDataSource = self.makeActiveAppsDataSource()
|
||||||
|
private lazy var inactiveAppsDataSource = self.makeInactiveAppsDataSource()
|
||||||
|
|
||||||
private var prototypeUpdateCell: UpdateCollectionViewCell!
|
private var prototypeUpdateCell: UpdateCollectionViewCell!
|
||||||
private var longPressGestureRecognizer: UILongPressGestureRecognizer!
|
|
||||||
private var sideloadingProgressView: UIProgressView!
|
private var sideloadingProgressView: UIProgressView!
|
||||||
|
|
||||||
// State
|
// State
|
||||||
@@ -89,6 +90,8 @@ class MyAppsViewController: UICollectionViewController
|
|||||||
|
|
||||||
self.collectionView.register(UpdateCollectionViewCell.nib, forCellWithReuseIdentifier: "UpdateCell")
|
self.collectionView.register(UpdateCollectionViewCell.nib, forCellWithReuseIdentifier: "UpdateCell")
|
||||||
self.collectionView.register(UpdatesCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "UpdatesHeader")
|
self.collectionView.register(UpdatesCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "UpdatesHeader")
|
||||||
|
self.collectionView.register(InstalledAppsCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "ActiveAppsHeader")
|
||||||
|
self.collectionView.register(InstalledAppsCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "InactiveAppsHeader")
|
||||||
|
|
||||||
self.sideloadingProgressView = UIProgressView(progressViewStyle: .bar)
|
self.sideloadingProgressView = UIProgressView(progressViewStyle: .bar)
|
||||||
self.sideloadingProgressView.translatesAutoresizingMaskIntoConstraints = false
|
self.sideloadingProgressView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
@@ -102,12 +105,6 @@ class MyAppsViewController: UICollectionViewController
|
|||||||
self.sideloadingProgressView.trailingAnchor.constraint(equalTo: navigationBar.trailingAnchor),
|
self.sideloadingProgressView.trailingAnchor.constraint(equalTo: navigationBar.trailingAnchor),
|
||||||
self.sideloadingProgressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)])
|
self.sideloadingProgressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestures
|
|
||||||
self.longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(MyAppsViewController.handleLongPressGesture(_:)))
|
|
||||||
self.collectionView.addGestureRecognizer(self.longPressGestureRecognizer)
|
|
||||||
|
|
||||||
self.registerForPreviewing(with: self, sourceView: self.collectionView)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool)
|
override func viewWillAppear(_ animated: Bool)
|
||||||
@@ -158,7 +155,7 @@ private extension MyAppsViewController
|
|||||||
{
|
{
|
||||||
func makeDataSource() -> RSTCompositeCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
|
func makeDataSource() -> RSTCompositeCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
|
||||||
{
|
{
|
||||||
let dataSource = RSTCompositeCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(dataSources: [self.noUpdatesDataSource, self.updatesDataSource, self.installedAppsDataSource])
|
let dataSource = RSTCompositeCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(dataSources: [self.noUpdatesDataSource, self.updatesDataSource, self.activeAppsDataSource, self.inactiveAppsDataSource])
|
||||||
dataSource.proxy = self
|
dataSource.proxy = self
|
||||||
return dataSource
|
return dataSource
|
||||||
}
|
}
|
||||||
@@ -261,9 +258,9 @@ private extension MyAppsViewController
|
|||||||
return dataSource
|
return dataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeInstalledAppsDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
|
func makeActiveAppsDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
|
||||||
{
|
{
|
||||||
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
let fetchRequest = InstalledApp.activeAppsFetchRequest()
|
||||||
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.storeApp)]
|
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.storeApp)]
|
||||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true),
|
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.expirationDate, ascending: true),
|
||||||
NSSortDescriptor(keyPath: \InstalledApp.refreshedDate, ascending: false),
|
NSSortDescriptor(keyPath: \InstalledApp.refreshedDate, ascending: false),
|
||||||
@@ -283,6 +280,9 @@ private extension MyAppsViewController
|
|||||||
cell.bannerView.iconImageView.isIndicatingActivity = true
|
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||||
cell.bannerView.betaBadgeView.isHidden = !(installedApp.storeApp?.isBeta ?? false)
|
cell.bannerView.betaBadgeView.isHidden = !(installedApp.storeApp?.isBeta ?? false)
|
||||||
|
|
||||||
|
cell.bannerView.buttonLabel.isHidden = false
|
||||||
|
cell.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
|
||||||
|
|
||||||
cell.bannerView.button.isIndicatingActivity = false
|
cell.bannerView.button.isIndicatingActivity = false
|
||||||
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.refreshApp(_:)), for: .primaryActionTriggered)
|
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.refreshApp(_:)), for: .primaryActionTriggered)
|
||||||
|
|
||||||
@@ -344,6 +344,67 @@ private extension MyAppsViewController
|
|||||||
return dataSource
|
return dataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeInactiveAppsDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
|
||||||
|
{
|
||||||
|
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
|
||||||
|
|
||||||
|
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||||
|
dataSource.cellIdentifierHandler = { _ in "AppCell" }
|
||||||
|
dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in
|
||||||
|
let tintColor = installedApp.storeApp?.tintColor ?? .altPrimary
|
||||||
|
|
||||||
|
let cell = cell as! InstalledAppCollectionViewCell
|
||||||
|
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||||
|
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
cell.tintColor = UIColor.gray
|
||||||
|
|
||||||
|
cell.bannerView.iconImageView.isIndicatingActivity = true
|
||||||
|
cell.bannerView.betaBadgeView.isHidden = !(installedApp.storeApp?.isBeta ?? false)
|
||||||
|
|
||||||
|
cell.bannerView.buttonLabel.isHidden = true
|
||||||
|
|
||||||
|
cell.bannerView.button.isIndicatingActivity = false
|
||||||
|
cell.bannerView.button.tintColor = tintColor
|
||||||
|
cell.bannerView.button.setTitle(NSLocalizedString("ACTIVATE", comment: ""), for: .normal)
|
||||||
|
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.activateApp(_:)), for: .primaryActionTriggered)
|
||||||
|
|
||||||
|
cell.bannerView.titleLabel.text = installedApp.name
|
||||||
|
cell.bannerView.subtitleLabel.text = installedApp.storeApp?.developerName ?? NSLocalizedString("Sideloaded", comment: "")
|
||||||
|
|
||||||
|
// Make sure refresh button is correct size.
|
||||||
|
cell.layoutIfNeeded()
|
||||||
|
|
||||||
|
// Ensure no leftover progress from active apps cell reuse.
|
||||||
|
cell.bannerView.button.progress = nil
|
||||||
|
}
|
||||||
|
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
||||||
|
let fileURL = item.fileURL
|
||||||
|
|
||||||
|
return BlockOperation {
|
||||||
|
guard let application = ALTApplication(fileURL: fileURL) else {
|
||||||
|
completion(nil, OperationError.invalidApp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon = application.icon
|
||||||
|
completion(icon, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
|
let cell = cell as! InstalledAppCollectionViewCell
|
||||||
|
cell.bannerView.iconImageView.image = image
|
||||||
|
cell.bannerView.iconImageView.isIndicatingActivity = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
|
||||||
func updateDataSource()
|
func updateDataSource()
|
||||||
{
|
{
|
||||||
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
|
||||||
@@ -433,10 +494,11 @@ private extension MyAppsViewController
|
|||||||
localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count))
|
localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count))
|
||||||
}
|
}
|
||||||
|
|
||||||
let detailText = failures.first?.value.localizedDescription
|
let error = failures.first?.value as NSError?
|
||||||
|
let detailText = error?.localizedFailureReason ?? error?.localizedDescription
|
||||||
|
|
||||||
toastView = ToastView(text: localizedText, detailText: detailText)
|
toastView = ToastView(text: localizedText, detailText: detailText)
|
||||||
toastView.preferredDuration = 2.0
|
toastView.preferredDuration = 4.0
|
||||||
}
|
}
|
||||||
|
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
@@ -449,7 +511,7 @@ private extension MyAppsViewController
|
|||||||
self.refreshGroup = group
|
self.refreshGroup = group
|
||||||
|
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation {
|
||||||
self.collectionView.reloadSections([Section.installedApps.rawValue])
|
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -525,24 +587,7 @@ private extension MyAppsViewController
|
|||||||
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
|
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
|
||||||
|
|
||||||
let installedApp = self.dataSource.item(at: indexPath)
|
let installedApp = self.dataSource.item(at: indexPath)
|
||||||
|
self.refresh(installedApp)
|
||||||
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
|
|
||||||
guard previousProgress == nil else {
|
|
||||||
previousProgress?.cancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.refresh([installedApp]) { (results) in
|
|
||||||
// If an error occured, reload the section so the progress bar is no longer visible.
|
|
||||||
if results.values.contains(where: { $0.error != nil })
|
|
||||||
{
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.collectionView.reloadSections(IndexSet(integer: Section.installedApps.rawValue))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Finished refreshing with results:", results.map { ($0, $1.error?.localizedDescription ?? "success") })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func refreshAllApps(_ sender: UIBarButtonItem)
|
@IBAction func refreshAllApps(_ sender: UIBarButtonItem)
|
||||||
@@ -555,7 +600,7 @@ private extension MyAppsViewController
|
|||||||
self.refresh(installedApps) { (result) in
|
self.refresh(installedApps) { (result) in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.isRefreshingAllApps = false
|
self.isRefreshingAllApps = false
|
||||||
self.collectionView.reloadSections(IndexSet(integer: Section.installedApps.rawValue))
|
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -705,7 +750,153 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func presentAlert(for installedApp: InstalledApp)
|
@IBAction func activateApp(_ sender: UIButton)
|
||||||
|
{
|
||||||
|
let point = self.collectionView.convert(sender.center, from: sender.superview)
|
||||||
|
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
|
||||||
|
|
||||||
|
let installedApp = self.dataSource.item(at: indexPath)
|
||||||
|
self.activate(installedApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func deactivateApp(_ sender: UIButton)
|
||||||
|
{
|
||||||
|
let point = self.collectionView.convert(sender.center, from: sender.superview)
|
||||||
|
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
|
||||||
|
|
||||||
|
let installedApp = self.dataSource.item(at: indexPath)
|
||||||
|
self.deactivate(installedApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func presentInactiveAppsAlert()
|
||||||
|
{
|
||||||
|
let alertController = UIAlertController(title: NSLocalizedString("What are inactive apps?", comment: ""), message: NSLocalizedString("Free developer accounts are limited to 3 apps and app extensions. Inactive apps don't count towards your total, but cannot be opened until activated.", comment: ""), preferredStyle: .alert)
|
||||||
|
alertController.addAction(.ok)
|
||||||
|
self.present(alertController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentDeactivateAppAlert(completionHandler: @escaping (Bool) -> Void)
|
||||||
|
{
|
||||||
|
let alertController = UIAlertController(title: NSLocalizedString("Cannot Activate More than 3 Apps", comment: ""), message: NSLocalizedString("Free developer accounts are limited to 3 active apps and app extensions. Please choose an app to deactivate.", comment: ""), preferredStyle: .alert)
|
||||||
|
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { (action) in
|
||||||
|
completionHandler(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
let activeApps = InstalledApp.fetchActiveApps(in: DatabaseManager.shared.viewContext)
|
||||||
|
for app in activeApps where app.bundleIdentifier != StoreApp.altstoreAppID
|
||||||
|
{
|
||||||
|
alertController.addAction(UIAlertAction(title: app.name, style: .default) { (action) in
|
||||||
|
self.deactivate(app) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure: completionHandler(false)
|
||||||
|
case .success: completionHandler(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
self.present(alertController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension MyAppsViewController
|
||||||
|
{
|
||||||
|
func refresh(_ installedApp: InstalledApp)
|
||||||
|
{
|
||||||
|
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
|
||||||
|
guard previousProgress == nil else {
|
||||||
|
previousProgress?.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.refresh([installedApp]) { (results) in
|
||||||
|
// If an error occured, reload the section so the progress bar is no longer visible.
|
||||||
|
if results.values.contains(where: { $0.error != nil })
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.collectionView.reloadSections([Section.activeApps.rawValue, Section.inactiveApps.rawValue])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Finished refreshing with results:", results.map { ($0, $1.error?.localizedDescription ?? "success") })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func activate(_ installedApp: InstalledApp)
|
||||||
|
{
|
||||||
|
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit
|
||||||
|
{
|
||||||
|
let activeApps = InstalledApp.fetchActiveApps(in: DatabaseManager.shared.viewContext)
|
||||||
|
let activeAppsCount = activeApps.reduce(0) { $0 + (1 + $1.appExtensions.count) } // As of iOS 13.3.1, app extensions count as "apps"
|
||||||
|
|
||||||
|
let availableActiveApps = max(sideloadedAppsLimit - activeAppsCount, 0)
|
||||||
|
let requiredActiveAppSlots = 1 + installedApp.appExtensions.count
|
||||||
|
|
||||||
|
guard requiredActiveAppSlots <= availableActiveApps else {
|
||||||
|
return self.presentDeactivateAppAlert { (shouldContinue) in
|
||||||
|
guard shouldContinue else { return }
|
||||||
|
|
||||||
|
installedApp.managedObjectContext?.perform {
|
||||||
|
self.activate(installedApp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !installedApp.isActive else { return }
|
||||||
|
installedApp.isActive = true
|
||||||
|
|
||||||
|
AppManager.shared.activate(installedApp, presentingViewController: self) { (result) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let app = try result.get()
|
||||||
|
try? app.managedObjectContext?.save()
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to activate app:", error)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
installedApp.isActive = false
|
||||||
|
|
||||||
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.show(in: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deactivate(_ installedApp: InstalledApp, completionHandler: ((Result<InstalledApp, Error>) -> Void)? = nil)
|
||||||
|
{
|
||||||
|
guard installedApp.isActive else { return }
|
||||||
|
installedApp.isActive = false
|
||||||
|
|
||||||
|
AppManager.shared.deactivate(installedApp) { (result) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let app = try result.get()
|
||||||
|
try? app.managedObjectContext?.save()
|
||||||
|
|
||||||
|
print("Finished deactivating app:", app.bundleIdentifier)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to activate app:", error)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
installedApp.isActive = true
|
||||||
|
|
||||||
|
let toastView = ToastView(error: error)
|
||||||
|
toastView.show(in: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler?(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(_ installedApp: InstalledApp)
|
||||||
{
|
{
|
||||||
let alertController = UIAlertController(title: nil, message: NSLocalizedString("Removing a sideloaded app only removes it from AltStore. You must also delete it from the home screen to fully uninstall the app.", comment: ""), preferredStyle: .actionSheet)
|
let alertController = UIAlertController(title: nil, message: NSLocalizedString("Removing a sideloaded app only removes it from AltStore. You must also delete it from the home screen to fully uninstall the app.", comment: ""), preferredStyle: .actionSheet)
|
||||||
alertController.addAction(.cancel)
|
alertController.addAction(.cancel)
|
||||||
@@ -738,30 +929,6 @@ private extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func handleLongPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer)
|
|
||||||
{
|
|
||||||
guard gestureRecognizer.state == .began else { return }
|
|
||||||
|
|
||||||
let point = gestureRecognizer.location(in: self.collectionView)
|
|
||||||
|
|
||||||
guard
|
|
||||||
let indexPath = self.collectionView.indexPathForItem(at: point),
|
|
||||||
indexPath.section == Section.installedApps.rawValue
|
|
||||||
else { return }
|
|
||||||
|
|
||||||
let installedApp = self.dataSource.item(at: indexPath)
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
self.presentAlert(for: installedApp)
|
|
||||||
#else
|
|
||||||
if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier)
|
|
||||||
{
|
|
||||||
// Only display alert for legacy sideloaded apps.
|
|
||||||
self.presentAlert(for: installedApp)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func importApp(_ notification: Notification)
|
@objc func importApp(_ notification: Notification)
|
||||||
{
|
{
|
||||||
// Make sure left UIBarButtonItem has been set.
|
// Make sure left UIBarButtonItem has been set.
|
||||||
@@ -842,24 +1009,54 @@ extension MyAppsViewController
|
|||||||
|
|
||||||
return headerView
|
return headerView
|
||||||
|
|
||||||
case .installedApps where kind == UICollectionView.elementKindSectionHeader:
|
case .activeApps where kind == UICollectionView.elementKindSectionHeader:
|
||||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "InstalledAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView
|
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "ActiveAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView
|
||||||
|
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation {
|
||||||
headerView.textLabel.text = NSLocalizedString("Installed", comment: "")
|
headerView.layoutMargins.left = self.view.layoutMargins.left
|
||||||
|
headerView.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
|
if UserDefaults.standard.activeAppsLimit == nil
|
||||||
|
{
|
||||||
|
headerView.textLabel.text = NSLocalizedString("Installed", comment: "")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
headerView.textLabel.text = NSLocalizedString("Active", comment: "")
|
||||||
|
}
|
||||||
|
|
||||||
headerView.button.isIndicatingActivity = false
|
headerView.button.isIndicatingActivity = false
|
||||||
headerView.button.activityIndicatorView.color = .altPrimary
|
headerView.button.activityIndicatorView.color = .altPrimary
|
||||||
headerView.button.setTitle(NSLocalizedString("Refresh All", comment: ""), for: .normal)
|
headerView.button.setTitle(NSLocalizedString("Refresh All", comment: ""), for: .normal)
|
||||||
headerView.button.addTarget(self, action: #selector(MyAppsViewController.refreshAllApps(_:)), for: .primaryActionTriggered)
|
headerView.button.addTarget(self, action: #selector(MyAppsViewController.refreshAllApps(_:)), for: .primaryActionTriggered)
|
||||||
headerView.button.isIndicatingActivity = self.isRefreshingAllApps
|
|
||||||
|
|
||||||
headerView.button.layoutIfNeeded()
|
headerView.button.layoutIfNeeded()
|
||||||
|
headerView.button.isIndicatingActivity = self.isRefreshingAllApps
|
||||||
}
|
}
|
||||||
|
|
||||||
return headerView
|
return headerView
|
||||||
|
|
||||||
case .installedApps:
|
case .inactiveApps where kind == UICollectionView.elementKindSectionHeader:
|
||||||
|
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "InactiveAppsHeader", for: indexPath) as! InstalledAppsCollectionHeaderView
|
||||||
|
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
headerView.layoutMargins.left = self.view.layoutMargins.left
|
||||||
|
headerView.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
|
headerView.textLabel.text = NSLocalizedString("Inactive", comment: "")
|
||||||
|
headerView.button.setTitle(nil, for: .normal)
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *)
|
||||||
|
{
|
||||||
|
headerView.button.setImage(UIImage(systemName: "questionmark.circle"), for: .normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
headerView.button.addTarget(self, action: #selector(MyAppsViewController.presentInactiveAppsAlert), for: .primaryActionTriggered)
|
||||||
|
}
|
||||||
|
|
||||||
|
return headerView
|
||||||
|
|
||||||
|
case .activeApps, .inactiveApps:
|
||||||
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "InstalledAppsFooter", for: indexPath) as! InstalledAppsCollectionFooterView
|
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "InstalledAppsFooter", for: indexPath) as! InstalledAppsCollectionFooterView
|
||||||
|
|
||||||
guard let team = DatabaseManager.shared.activeTeam() else { return footerView }
|
guard let team = DatabaseManager.shared.activeTeam() else { return footerView }
|
||||||
@@ -904,6 +1101,102 @@ extension MyAppsViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@available(iOS 13.0, *)
|
||||||
|
extension MyAppsViewController
|
||||||
|
{
|
||||||
|
private func actions(for installedApp: InstalledApp) -> [UIAction]
|
||||||
|
{
|
||||||
|
var actions = [UIAction]()
|
||||||
|
|
||||||
|
let refreshAction = UIAction(title: NSLocalizedString("Refresh", comment: ""), image: UIImage(systemName: "arrow.clockwise")) { (action) in
|
||||||
|
self.refresh(installedApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
let activateAction = UIAction(title: NSLocalizedString("Activate", comment: ""), image: UIImage(systemName: "checkmark.circle")) { (action) in
|
||||||
|
self.activate(installedApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
let deactivateAction = UIAction(title: NSLocalizedString("Deactivate", comment: ""), image: UIImage(systemName: "xmark.circle"), attributes: .destructive) { (action) in
|
||||||
|
self.deactivate(installedApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
let removeAction = UIAction(title: NSLocalizedString("Remove", comment: ""), image: UIImage(systemName: "trash"), attributes: .destructive) { (action) in
|
||||||
|
self.remove(installedApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if installedApp.bundleIdentifier == StoreApp.altstoreAppID
|
||||||
|
{
|
||||||
|
actions = [refreshAction]
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if installedApp.isActive
|
||||||
|
{
|
||||||
|
if UserDefaults.standard.activeAppsLimit != nil
|
||||||
|
{
|
||||||
|
actions = [refreshAction, deactivateAction]
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
actions = [refreshAction]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
actions.append(activateAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
actions.append(removeAction)
|
||||||
|
#else
|
||||||
|
if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier)
|
||||||
|
{
|
||||||
|
// Only display option for legacy sideloaded apps.
|
||||||
|
actions.append(removeAction)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
|
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration?
|
||||||
|
{
|
||||||
|
let section = Section(rawValue: indexPath.section)!
|
||||||
|
switch section
|
||||||
|
{
|
||||||
|
case .updates, .noUpdates: return nil
|
||||||
|
case .activeApps, .inactiveApps:
|
||||||
|
let installedApp = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { (suggestedActions) -> UIMenu? in
|
||||||
|
let actions = self.actions(for: installedApp)
|
||||||
|
|
||||||
|
let menu = UIMenu(title: "", children: actions)
|
||||||
|
return menu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview?
|
||||||
|
{
|
||||||
|
guard let indexPath = configuration.identifier as? NSIndexPath else { return nil }
|
||||||
|
guard let cell = collectionView.cellForItem(at: indexPath as IndexPath) as? InstalledAppCollectionViewCell else { return nil }
|
||||||
|
|
||||||
|
let parameters = UIPreviewParameters()
|
||||||
|
parameters.backgroundColor = .clear
|
||||||
|
parameters.visiblePath = UIBezierPath(roundedRect: cell.bannerView.bounds, cornerRadius: cell.bannerView.layer.cornerRadius)
|
||||||
|
|
||||||
|
let preview = UITargetedPreview(view: cell.bannerView, parameters: parameters)
|
||||||
|
return preview
|
||||||
|
}
|
||||||
|
|
||||||
|
override func collectionView(_ collectionView: UICollectionView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview?
|
||||||
|
{
|
||||||
|
return self.collectionView(collectionView, previewForHighlightingContextMenuWithConfiguration: configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension MyAppsViewController: UICollectionViewDelegateFlowLayout
|
extension MyAppsViewController: UICollectionViewDelegateFlowLayout
|
||||||
{
|
{
|
||||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||||
@@ -936,7 +1229,7 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
|
|||||||
self.cachedUpdateSizes[item.bundleIdentifier] = size
|
self.cachedUpdateSizes[item.bundleIdentifier] = size
|
||||||
return size
|
return size
|
||||||
|
|
||||||
case .installedApps:
|
case .activeApps, .inactiveApps:
|
||||||
return CGSize(width: collectionView.bounds.width, height: 88)
|
return CGSize(width: collectionView.bounds.width, height: 88)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -951,18 +1244,18 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
|
|||||||
let height: CGFloat = self.updatesDataSource.itemCount > maximumCollapsedUpdatesCount ? 26 : 0
|
let height: CGFloat = self.updatesDataSource.itemCount > maximumCollapsedUpdatesCount ? 26 : 0
|
||||||
return CGSize(width: collectionView.bounds.width, height: height)
|
return CGSize(width: collectionView.bounds.width, height: height)
|
||||||
|
|
||||||
case .installedApps: return CGSize(width: collectionView.bounds.width, height: 29)
|
case .activeApps: return CGSize(width: collectionView.bounds.width, height: 29)
|
||||||
|
case .inactiveApps where self.inactiveAppsDataSource.itemCount == 0: return .zero
|
||||||
|
case .inactiveApps: return CGSize(width: collectionView.bounds.width, height: 29)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
|
||||||
{
|
{
|
||||||
let section = Section.allCases[section]
|
let section = Section.allCases[section]
|
||||||
switch section
|
|
||||||
|
func appIDsFooterSize() -> CGSize
|
||||||
{
|
{
|
||||||
case .noUpdates: return .zero
|
|
||||||
case .updates: return .zero
|
|
||||||
case .installedApps:
|
|
||||||
#if BETA
|
#if BETA
|
||||||
guard let _ = DatabaseManager.shared.activeTeam() else { return .zero }
|
guard let _ = DatabaseManager.shared.activeTeam() else { return .zero }
|
||||||
|
|
||||||
@@ -977,6 +1270,18 @@ extension MyAppsViewController: UICollectionViewDelegateFlowLayout
|
|||||||
return .zero
|
return .zero
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch section
|
||||||
|
{
|
||||||
|
case .noUpdates: return .zero
|
||||||
|
case .updates: return .zero
|
||||||
|
|
||||||
|
case .activeApps where self.inactiveAppsDataSource.itemCount == 0: return appIDsFooterSize()
|
||||||
|
case .activeApps: return .zero
|
||||||
|
|
||||||
|
case .inactiveApps where self.inactiveAppsDataSource.itemCount == 0: return .zero
|
||||||
|
case .inactiveApps: return appIDsFooterSize()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ myCV: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
|
func collectionView(_ myCV: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
|
||||||
|
|||||||
@@ -228,6 +228,17 @@ class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppl
|
|||||||
team.isActiveTeam = false
|
team.isActiveTeam = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1)
|
||||||
|
if team.type == .free, ProcessInfo.processInfo.isOperatingSystemAtLeast(activeAppsMinimumVersion)
|
||||||
|
{
|
||||||
|
// Free developer accounts are limited to only 3 active sideloaded apps at a time as of iOS 13.3.1.
|
||||||
|
UserDefaults.standard.activeAppsLimit = 3
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UserDefaults.standard.activeAppsLimit = nil
|
||||||
|
}
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
try context.save()
|
try context.save()
|
||||||
|
|
||||||
|
|||||||
90
AltStore/Operations/DeactivateAppOperation.swift
Normal file
90
AltStore/Operations/DeactivateAppOperation.swift
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
//
|
||||||
|
// DeactivateAppOperation.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 3/4/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
import AltSign
|
||||||
|
import AltKit
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
@objc(DeactivateAppOperation)
|
||||||
|
class DeactivateAppOperation: ResultOperation<InstalledApp>
|
||||||
|
{
|
||||||
|
let app: InstalledApp
|
||||||
|
let context: OperationContext
|
||||||
|
|
||||||
|
init(app: InstalledApp, context: OperationContext)
|
||||||
|
{
|
||||||
|
self.app = app
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func main()
|
||||||
|
{
|
||||||
|
super.main()
|
||||||
|
|
||||||
|
if let error = self.context.error
|
||||||
|
{
|
||||||
|
self.finish(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) }
|
||||||
|
guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return self.finish(.failure(OperationError.unknownUDID)) }
|
||||||
|
|
||||||
|
ServerManager.shared.connect(to: server) { (result) in
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): self.finish(.failure(error))
|
||||||
|
case .success(let connection):
|
||||||
|
print("Sending deactivate app request...")
|
||||||
|
|
||||||
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
|
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
|
||||||
|
|
||||||
|
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
|
||||||
|
let allIdentifiers = [installedApp.resignedBundleIdentifier] + appExtensionProfiles
|
||||||
|
|
||||||
|
let request = RemoveProvisioningProfilesRequest(udid: udid, bundleIdentifiers: Set(allIdentifiers))
|
||||||
|
connection.send(request) { (result) in
|
||||||
|
print("Sent deactive app request!")
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): self.finish(.failure(error))
|
||||||
|
case .success:
|
||||||
|
print("Waiting for deactivate app response...")
|
||||||
|
connection.receiveResponse() { (result) in
|
||||||
|
print("Receiving deactivate app response:", result)
|
||||||
|
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(let error): self.finish(.failure(error))
|
||||||
|
case .success(.error(let response)): self.finish(.failure(response.error))
|
||||||
|
case .success(.removeProvisioningProfiles):
|
||||||
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
|
self.progress.completedUnitCount += 1
|
||||||
|
|
||||||
|
let installedApp = context.object(with: self.app.objectID) as! InstalledApp
|
||||||
|
installedApp.isActive = false
|
||||||
|
self.finish(.success(installedApp))
|
||||||
|
}
|
||||||
|
|
||||||
|
case .success: self.finish(.failure(ALTServerError(.unknownResponse)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,7 +111,40 @@ class InstallAppOperation: ResultOperation<InstalledApp>
|
|||||||
|
|
||||||
self.context.beginInstallationHandler?(installedApp)
|
self.context.beginInstallationHandler?(installedApp)
|
||||||
|
|
||||||
let request = BeginInstallationRequest()
|
var activeProfiles: Set<String>?
|
||||||
|
if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit
|
||||||
|
{
|
||||||
|
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
|
||||||
|
|
||||||
|
let fetchRequest = InstalledApp.activeAppsFetchRequest()
|
||||||
|
fetchRequest.includesPendingChanges = false
|
||||||
|
|
||||||
|
var activeApps = InstalledApp.fetch(fetchRequest, in: backgroundContext)
|
||||||
|
if !activeApps.contains(installedApp)
|
||||||
|
{
|
||||||
|
let availableActiveApps = max(sideloadedAppsLimit - activeApps.count, 0)
|
||||||
|
let requiredActiveAppSlots = 1 + installedExtensions.count // As of iOS 13.3.1, app extensions count as "apps"
|
||||||
|
|
||||||
|
if requiredActiveAppSlots <= availableActiveApps
|
||||||
|
{
|
||||||
|
// This app has not been explicitly activated, but there are enough slots available,
|
||||||
|
// so implicitly activate it.
|
||||||
|
installedApp.isActive = true
|
||||||
|
activeApps.append(installedApp)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
installedApp.isActive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
|
||||||
|
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
|
||||||
|
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = BeginInstallationRequest(activeProfiles: activeProfiles)
|
||||||
connection.send(request) { (result) in
|
connection.send(request) { (result) in
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,10 +58,10 @@ class RefreshAppOperation: ResultOperation<InstalledApp>
|
|||||||
print("Sending refresh app request...")
|
print("Sending refresh app request...")
|
||||||
|
|
||||||
var activeProfiles: Set<String>?
|
var activeProfiles: Set<String>?
|
||||||
|
if UserDefaults.standard.activeAppsLimit != nil
|
||||||
if team.type == .free
|
|
||||||
{
|
{
|
||||||
let activeApps = InstalledApp.all(in: context)
|
// When installing these new profiles, AltServer will remove all non-active profiles to ensure we remain under limit.
|
||||||
|
let activeApps = InstalledApp.fetchActiveApps(in: context)
|
||||||
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
|
activeProfiles = Set(activeApps.flatMap { (installedApp) -> [String] in
|
||||||
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
|
let appExtensionProfiles = installedApp.appExtensions.map { $0.resignedBundleIdentifier }
|
||||||
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
|
return [installedApp.resignedBundleIdentifier] + appExtensionProfiles
|
||||||
|
|||||||
@@ -26,6 +26,20 @@ extension Fetchable
|
|||||||
return managedObjects
|
return managedObjects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func fetch(_ fetchRequest: NSFetchRequest<Self>, in context: NSManagedObjectContext) -> [Self]
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let managedObjects = try context.fetch(fetchRequest)
|
||||||
|
return managedObjects
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
print("Failed to fetch managed objects. Fetch Request: \(fetchRequest). Error: \(error).")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static func all(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext, returnFirstResult: Bool) -> [Self]
|
private static func all(satisfying predicate: NSPredicate? = nil, sortedBy sortDescriptors: [NSSortDescriptor]? = nil, in context: NSManagedObjectContext, returnFirstResult: Bool) -> [Self]
|
||||||
{
|
{
|
||||||
let registeredObjects = context.registeredObjects.lazy.compactMap({ $0 as? Self }).filter({ predicate?.evaluate(with: $0) != false })
|
let registeredObjects = context.registeredObjects.lazy.compactMap({ $0 as? Self }).filter({ predicate?.evaluate(with: $0) != false })
|
||||||
@@ -39,23 +53,15 @@ extension Fetchable
|
|||||||
fetchRequest.predicate = predicate
|
fetchRequest.predicate = predicate
|
||||||
fetchRequest.sortDescriptors = sortDescriptors
|
fetchRequest.sortDescriptors = sortDescriptors
|
||||||
|
|
||||||
do
|
let fetchedObjects = self.fetch(fetchRequest, in: context)
|
||||||
{
|
|
||||||
let managedObjects = try context.fetch(fetchRequest)
|
|
||||||
|
|
||||||
if let managedObject = managedObjects.first, returnFirstResult
|
if let fetchedObject = fetchedObjects.first, returnFirstResult
|
||||||
{
|
|
||||||
return [managedObject]
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return managedObjects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
{
|
||||||
print("Failed to fetch managed objects.", error)
|
return [fetchedObject]
|
||||||
return []
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return fetchedObjects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user