diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 6fbd6c5b..fee5ac40 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -21,7 +21,9 @@ jobs:
steps:
- name: Set current build as BETA
- run: echo "IS_BETA=1" >> $GITHUB_ENV
+ run: |
+ echo "IS_BETA=1" >> $GITHUB_ENV
+ echo "RELEASE_CHANNEL=beta" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
@@ -133,14 +135,40 @@ jobs:
- name: Build SideStore
- run: NSUnbufferedIO=YES make build 2>&1 | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
+ # using 'tee' to intercept stdout and log for detailed build-log
+ run: |
+ NSUnbufferedIO=YES make build 2>&1 | tee build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: Fakesign app
- run: make fakesign
+ run: make fakesign | tee -a build.log
- name: Convert to IPA
- run: make ipa
+ run: make ipa | tee -a build.log
+ - name: Encrypt build.log generated from SideStore build for upload
+ run: |
+ DEFAULT_BUILD_LOG_PASSWORD=12345
+
+ BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
+ BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
+
+ if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
+ echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
+ fi
+
+ if [ ! -f build.log ]; then
+ echo "Warning: build.log is missing, creating a dummy log..."
+ echo "Error: build.log was missing, This is a dummy placeholder file..." > build.log
+ fi
+
+ zip -e -P "$BUILD_LOG_ZIP_PASSWORD" encrypted-build_log.zip build.log
+
+ - name: List Files after SideStore build
+ run: |
+ echo ">>>>>>>>> Workdir <<<<<<<<<<"
+ ls -la .
+ echo ""
+
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
@@ -159,7 +187,7 @@ jobs:
release: "Nightly"
tag: "nightly"
prerelease: true
- files: SideStore.ipa SideStore.dSYMs.zip
+ files: SideStore.ipa SideStore.dSYMs.zip encrypted-build_log.zip
body: |
This is an ⚠️ **EXPERIMENTAL** ⚠️ nightly build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
@@ -189,6 +217,12 @@ jobs:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./SideStore.xcarchive/dSYMs/*
+ - name: Upload encrypted-build_log.zip
+ uses: actions/upload-artifact@v4
+ with:
+ name: encrypted-build_log.zip
+ path: encrypted-build_log.zip
+
# Check if PUBLISH_BETA_UPDATES secret is set to non-zero
- name: Check if PUBLISH_BETA_UPDATES is set
id: check_publish
@@ -242,44 +276,42 @@ jobs:
run: |
echo "VERSION_IPA=$VERSION_IPA" >> $GITHUB_ENV
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
- echo "BETA=true" >> $GITHUB_ENV
echo "COMMIT_ID=$SHORT_COMMIT" >> $GITHUB_ENV
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
echo "LOCALIZED_DESCRIPTION=This is nightly release for revision: ${{ github.sha }}" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/nightly/SideStore.ipa" >> $GITHUB_ENV
- # - name: Checkout SideStore/apps-v2.json
- # if: ${{ steps.check_publish.outcome == 'success' }}
- # uses: actions/checkout@v4
- # with:
- # # Repository name with owner. For example, actions/checkout
- # # Default: ${{ github.repository }}
- # repository: 'SideStore/apps-v2.json'
- # # ref: 'main' # TODO: use branches for alpha and beta tracks? so as to avoid push collision?
- # ref: 'nightly' # TODO: use branches for alpha and beta tracks? so as to avoid push collision?
- # # token: ${{ github.token }}
- # token: ${{ secrets.APPS_DEPLOY_KEY }}
- # path: 'SideStore/apps-v2.json'
+ - name: Checkout SideStore/apps-v2.json
+ if: ${{ steps.check_publish.outcome == 'success' }}
+ uses: actions/checkout@v4
+ with:
+ # Repository name with owner. For example, actions/checkout
+ # Default: ${{ github.repository }}
+ repository: 'SideStore/apps-v2.json'
+ ref: 'main' # TODO: use branches for alpha and beta tracks? so as to avoid push collision?
+ # ref: 'nightly' # TODO: use branches for alpha and beta tracks? so as to avoid push collision?
+ # token: ${{ github.token }}
+ token: ${{ secrets.APPS_DEPLOY_KEY }}
+ path: 'SideStore/apps-v2.json'
- # - name: Publish to SideStore/apps-v2.json
- # if: ${{ steps.check_publish.outcome == 'success' }}
- # run: |
- # # Copy and execute the update script
- # pushd SideStore/apps-v2.json/
+ - name: Publish to SideStore/apps-v2.json
+ if: ${{ steps.check_publish.outcome == 'success' }}
+ run: |
+ # Copy and execute the update script
+ pushd SideStore/apps-v2.json/
- # # Configure Git user (committer details)
- # git config user.name "GitHub Actions"
- # git config user.email "github-actions@github.com"
+ # Configure Git user (committer details)
+ git config user.name "GitHub Actions"
+ git config user.email "github-actions@github.com"
- # # Make the update script executable and run it
- # python3 ../../update_apps.py "./_includes/source.json"
+ # update the source.json
+ python3 ../../update_apps.py "./_includes/source.json"
- # # Commit changes and push using SSH
- # git add ./_includes/source.json
- # git commit -m " - updated for $SHORT_COMMIT deployment" || echo "No changes to commit"
+ # Commit changes and push using SSH
+ git add ./_includes/source.json
+ git commit -m " - updated for $SHORT_COMMIT deployment" || echo "No changes to commit"
- # git status
- # # git push origin HEAD:main
- # git push origin HEAD:nightly
- # popd
+ git status
+ git push origin HEAD:main
+ popd
diff --git a/.gitignore b/.gitignore
index 2fcc4bde..d7f580cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
# macOS
#
-*.DS_Store
+**/*.DS_Store
# Xcode
#
diff --git a/AltBackup/ViewController.swift b/AltBackup/ViewController.swift
index eca547bb..c0cf2565 100644
--- a/AltBackup/ViewController.swift
+++ b/AltBackup/ViewController.swift
@@ -82,7 +82,7 @@ class ViewController: UIViewController
self.activityIndicatorView.color = .altstoreText
self.activityIndicatorView.startAnimating()
- // TODO: @mahee96: Disabled this buttons which were present for debugging purpose.
+ // TODO: @mahee96: Disabled these backup/restore buttons in altbackup.app screen which were present for debugging purpose.
// Can find something useful for these later, but these are not required by this backup/restore app
// #if DEBUG
// let button1 = UIButton(type: .system)
diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj
index 87e53fa4..914ff9a8 100644
--- a/AltStore.xcodeproj/project.pbxproj
+++ b/AltStore.xcodeproj/project.pbxproj
@@ -9,7 +9,6 @@
/* Begin PBXBuildFile section */
03F06CD52942C27E001C4D68 /* Bundle+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */; };
0E05025C2BEC947000879B5C /* String+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E05025B2BEC947000879B5C /* String+SideStore.swift */; };
- 0E13E5862CC8F55900E9C0DF /* ProcessInfo+SideStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E13E5852CC8F55900E9C0DF /* ProcessInfo+SideStore.swift */; };
0E1A1F912AE36A9700364CAD /* bytearray.c in Sources */ = {isa = PBXBuildFile; fileRef = 0E1A1F902AE36A9600364CAD /* bytearray.c */; };
0EA1665B2ADFE0D2003015C1 /* out-limd.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166472ADFE0D1003015C1 /* out-limd.c */; };
0EA1665C2ADFE0D2003015C1 /* out-default.c in Sources */ = {isa = PBXBuildFile; fileRef = 0EA166522ADFE0D2003015C1 /* out-default.c */; };
@@ -50,17 +49,37 @@
551A15E55999499418AC1022 /* Pods_AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 707E746318F0B6F1A44935D3 /* Pods_AltStoreCore.framework */; };
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A800F7032CE28E2F00208744 /* View+AltWidget.swift */; };
A805C3CD2D0C316A00E76BDD /* Pods_SideStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */; };
+ A8087E752D2D2958002DB21B /* ImportExport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8087E742D2D2958002DB21B /* ImportExport.swift */; };
+ A8096D182D30AD4F000C39C6 /* WidgetUpdateIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */; };
+ A8096D1C2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */; };
A809F69E2D04D7AC00F0F0F3 /* libminimuxer_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F68E2D04D71200F0F0F3 /* libminimuxer_static.a */; };
A809F69F2D04D7B300F0F0F3 /* libem_proxy_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A809F6942D04D71200F0F0F3 /* libem_proxy_static.a */; };
A809F6A82D04DA1900F0F0F3 /* minimuxer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */; };
A809F6A92D04DA1900F0F0F3 /* SwiftBridgeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */; };
+ A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */; };
+ A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */; };
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */; };
A82067C42D03E0DE00645C0D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = A82067C32D03E0DE00645C0D /* SemanticVersion */; };
A859ED5C2D1EE827003DCC58 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
A859ED5D2D1EE827003DCC58 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A86315DF2D3EB2DE0048FA40 /* ErrorProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */; };
+ A868CFE42D31999A002F1201 /* SingletonGenericMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A868CFE32D319988002F1201 /* SingletonGenericMap.swift */; };
+ A8696EE42D34512C00E96389 /* RemoveAppExtensionsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */; };
+ A88B8C492D35AD3200F53F9D /* OperationsLoggingContolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */; };
+ A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */; };
A8945AA62D059B6100D86CBE /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8945AA52D059B6100D86CBE /* Roxas.framework */; };
A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A5432F2D04F0C100D72399 /* libfragmentzip.a */; };
+ A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */; };
+ A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */; };
+ A8B516E32D2666CA0047047C /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E22D2666CA0047047C /* CoreDataHelper.swift */; };
+ A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E52D2668020047047C /* DateTimeUtil.swift */; };
A8BB34E52D04EC8E000A8B4D /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */; };
+ A8C38C242D206A3A00E83DBD /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */; };
+ A8C38C262D206A3A00E83DBD /* ConsoleLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */; };
+ A8C38C2A2D206AC100E83DBD /* OutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C282D206AC100E83DBD /* OutputStream.swift */; };
+ A8C38C2C2D206AD900E83DBD /* AbstractClassError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C2B2D206AD900E83DBD /* AbstractClassError.swift */; };
+ A8C38C322D206B2500E83DBD /* FileOutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C312D206B2500E83DBD /* FileOutputStream.swift */; };
+ A8C38C382D2084D000E83DBD /* ConsoleLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */; };
A8C6D50C2D1EE87600DF01F1 /* AltSign-Static in Frameworks */ = {isa = PBXBuildFile; productRef = A8C6D50B2D1EE87600DF01F1 /* AltSign-Static */; };
A8C6D5122D1EE8AF00DF01F1 /* AltSign-Static in Frameworks */ = {isa = PBXBuildFile; productRef = A8C6D5112D1EE8AF00DF01F1 /* AltSign-Static */; };
A8C6D5132D1EE8D700DF01F1 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
@@ -68,6 +87,7 @@
A8C6D5172D1EE95B00DF01F1 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
A8C6D5182D1EE95B00DF01F1 /* OpenSSL.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
A8D484D82D0CD306002C691D /* AltBackup.ipa in Resources */ = {isa = PBXBuildFile; fileRef = A8D484D72D0CD306002C691D /* AltBackup.ipa */; };
+ A8D49F532D3D2F9400844B92 /* ProcessInfo+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */; };
A8F838922D048E8F00ED425D /* libEmotionalDamage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19104DB22909C06C00C49C7B /* libEmotionalDamage.a */; };
A8F838932D048E8F00ED425D /* libminimuxer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 191E5FAB290A5D92001A3B7C /* libminimuxer.a */; };
A8F838942D048ECE00ED425D /* libimobiledevice.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF45872B2298D31600BD7491 /* libimobiledevice.a */; };
@@ -555,9 +575,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
- 0E0502592BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = ""; };
0E05025B2BEC947000879B5C /* String+SideStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SideStore.swift"; sourceTree = ""; };
- 0E13E5852CC8F55900E9C0DF /* ProcessInfo+SideStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+SideStore.swift"; sourceTree = ""; };
0E1A1F902AE36A9600364CAD /* bytearray.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = bytearray.c; path = src/bytearray.c; sourceTree = ""; };
0EA166412ADFE0D1003015C1 /* jplist.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = jplist.c; path = Dependencies/libplist/src/jplist.c; sourceTree = SOURCE_ROOT; };
0EA166422ADFE0D1003015C1 /* Date.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Date.cpp; path = Dependencies/libplist/src/Date.cpp; sourceTree = SOURCE_ROOT; };
@@ -608,12 +626,17 @@
7935E4499B2FC11DA8BAB2CC /* Pods-AltStoreCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStoreCore.release.xcconfig"; path = "Target Support Files/Pods-AltStoreCore/Pods-AltStoreCore.release.xcconfig"; sourceTree = ""; };
A800F7032CE28E2F00208744 /* View+AltWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+AltWidget.swift"; sourceTree = ""; };
A805C3CC2D0C316A00E76BDD /* Pods_SideStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Pods_SideStore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A8087E742D2D2958002DB21B /* ImportExport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExport.swift; sourceTree = ""; };
+ A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetUpdateIntent.swift; sourceTree = ""; };
+ A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActiveAppsTimelineProvider+Simulator.swift"; sourceTree = ""; };
A809F6A22D04DA1900F0F0F3 /* minimuxer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = minimuxer.h; sourceTree = ""; };
A809F6A32D04DA1900F0F0F3 /* minimuxer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = minimuxer.swift; sourceTree = ""; };
A809F6A42D04DA1900F0F0F3 /* minimuxer-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "minimuxer-Bridging-Header.h"; sourceTree = ""; };
A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "minimuxer-helpers.swift"; sourceTree = ""; };
A809F6A62D04DA1900F0F0F3 /* SwiftBridgeCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftBridgeCore.h; sourceTree = ""; };
A809F6A72D04DA1900F0F0F3 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftBridgeCore.swift; sourceTree = ""; };
+ A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIntent.swift; sourceTree = ""; };
+ A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationDataHolder.swift; sourceTree = ""; };
A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:67RAULRX93:Marcin Krzyzanowski"; lastKnownFileType = wrapper.xcframework; name = OpenSSL.xcframework; path = SideStore/AltSign/Dependencies/OpenSSL/Frameworks/OpenSSL.xcframework; sourceTree = ""; };
A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltBackup.xcconfig; sourceTree = ""; };
A85ACB8F2D1F31C400AA3DE7 /* AltStore.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.debug.xcconfig; sourceTree = ""; };
@@ -623,8 +646,24 @@
A85ACB932D1F31C400AA3DE7 /* AltWidgetExtension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltWidgetExtension.xcconfig; sourceTree = ""; };
A86202322D1F35640091187B /* AltStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.xcconfig; sourceTree = ""; };
A86202332D1F35640091187B /* AltStoreCore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStoreCore.xcconfig; sourceTree = ""; };
+ A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorProcessing.swift; sourceTree = ""; };
+ A868CFE32D319988002F1201 /* SingletonGenericMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingletonGenericMap.swift; sourceTree = ""; };
+ A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveAppExtensionsOperation.swift; sourceTree = ""; };
+ A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsLoggingContolView.swift; sourceTree = ""; };
+ A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsLoggingControl.swift; sourceTree = ""; };
A8945AA52D059B6100D86CBE /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveAppsTimelineProvider.swift; sourceTree = ""; };
+ A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInfoManager.swift; sourceTree = ""; };
+ A8B516E22D2666CA0047047C /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = ""; };
+ A8B516E52D2668020047047C /* DateTimeUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeUtil.swift; sourceTree = ""; };
+ A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = ""; };
+ A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLog.swift; sourceTree = ""; };
+ A8C38C282D206AC100E83DBD /* OutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputStream.swift; sourceTree = ""; };
+ A8C38C2B2D206AD900E83DBD /* AbstractClassError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractClassError.swift; sourceTree = ""; };
+ A8C38C312D206B2500E83DBD /* FileOutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOutputStream.swift; sourceTree = ""; };
+ A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogView.swift; sourceTree = ""; };
A8D484D72D0CD306002C691D /* AltBackup.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; path = AltBackup.ipa; sourceTree = ""; };
+ A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+AltStore.swift"; sourceTree = ""; };
A8F66C3C2D04D433009689E6 /* em_proxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = em_proxy.h; sourceTree = ""; };
A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = minimuxer.xcodeproj; sourceTree = ""; };
A8FD915B2D046EF100322782 /* ProcessError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessError.swift; sourceTree = ""; };
@@ -1118,6 +1157,24 @@
path = Extensions;
sourceTree = "";
};
+ A8087E712D2D291B002DB21B /* importexport */ = {
+ isa = PBXGroup;
+ children = (
+ A8087E742D2D2958002DB21B /* ImportExport.swift */,
+ );
+ path = importexport;
+ sourceTree = "";
+ };
+ A8096D1D2D30ADD5000C39C6 /* Providers */ = {
+ isa = PBXGroup;
+ children = (
+ D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */,
+ A8A853AE2D3065A300995795 /* ActiveAppsTimelineProvider.swift */,
+ A8096D1B2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift */,
+ );
+ path = Providers;
+ sourceTree = "";
+ };
A809F68A2D04D71200F0F0F3 /* Products */ = {
isa = PBXGroup;
children = (
@@ -1135,6 +1192,15 @@
name = Products;
sourceTree = "";
};
+ A80D790B2D2F209700A40F40 /* Intents */ = {
+ isa = PBXGroup;
+ children = (
+ A80D790C2D2F20AF00A40F40 /* PaginationIntent.swift */,
+ A8096D172D30AD4F000C39C6 /* WidgetUpdateIntent.swift */,
+ );
+ path = Intents;
+ sourceTree = "";
+ };
A85ACB942D1F31C400AA3DE7 /* xcconfigs */ = {
isa = PBXGroup;
children = (
@@ -1150,6 +1216,22 @@
path = xcconfigs;
sourceTree = "";
};
+ A86315DD2D3EB2BD0048FA40 /* errors */ = {
+ isa = PBXGroup;
+ children = (
+ A86315DE2D3EB2D80048FA40 /* ErrorProcessing.swift */,
+ );
+ path = errors;
+ sourceTree = "";
+ };
+ A88B8C532D35F1E800F53F9D /* operations */ = {
+ isa = PBXGroup;
+ children = (
+ A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */,
+ );
+ path = operations;
+ sourceTree = "";
+ };
A8A543222D04F0C100D72399 /* Products */ = {
isa = PBXGroup;
children = (
@@ -1161,6 +1243,81 @@
name = Products;
sourceTree = "";
};
+ A8A853AD2D3050CC00995795 /* pagination */ = {
+ isa = PBXGroup;
+ children = (
+ A80D790E2D2F217000A40F40 /* PaginationDataHolder.swift */,
+ );
+ path = pagination;
+ sourceTree = "";
+ };
+ A8AD35562D31BE8F003A28B4 /* Manager */ = {
+ isa = PBXGroup;
+ children = (
+ A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */,
+ );
+ path = Manager;
+ sourceTree = "";
+ };
+ A8AD35572D31BEB2003A28B4 /* datastructures */ = {
+ isa = PBXGroup;
+ children = (
+ A868CFE32D319988002F1201 /* SingletonGenericMap.swift */,
+ );
+ path = datastructures;
+ sourceTree = "";
+ };
+ A8B516DE2D2666900047047C /* dignostics */ = {
+ isa = PBXGroup;
+ children = (
+ A86315DD2D3EB2BD0048FA40 /* errors */,
+ A88B8C532D35F1E800F53F9D /* operations */,
+ A8B516DF2D2666A00047047C /* database */,
+ );
+ path = dignostics;
+ sourceTree = "";
+ };
+ A8B516DF2D2666A00047047C /* database */ = {
+ isa = PBXGroup;
+ children = (
+ A8B516E22D2666CA0047047C /* CoreDataHelper.swift */,
+ );
+ path = database;
+ sourceTree = "";
+ };
+ A8C38C1C2D2068D100E83DBD /* Utils */ = {
+ isa = PBXGroup;
+ children = (
+ A8AD35572D31BEB2003A28B4 /* datastructures */,
+ A8A853AD2D3050CC00995795 /* pagination */,
+ A8087E712D2D291B002DB21B /* importexport */,
+ A8B516DE2D2666900047047C /* dignostics */,
+ A8C38C272D206AA500E83DBD /* common */,
+ A8C38C202D206A3A00E83DBD /* iostreams */,
+ );
+ path = Utils;
+ sourceTree = "";
+ };
+ A8C38C202D206A3A00E83DBD /* iostreams */ = {
+ isa = PBXGroup;
+ children = (
+ A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */,
+ A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */,
+ );
+ path = iostreams;
+ sourceTree = "";
+ };
+ A8C38C272D206AA500E83DBD /* common */ = {
+ isa = PBXGroup;
+ children = (
+ A8B516E52D2668020047047C /* DateTimeUtil.swift */,
+ A8C38C282D206AC100E83DBD /* OutputStream.swift */,
+ A8C38C312D206B2500E83DBD /* FileOutputStream.swift */,
+ A8C38C2B2D206AD900E83DBD /* AbstractClassError.swift */,
+ );
+ path = common;
+ sourceTree = "";
+ };
A8F66C072D04C025009689E6 /* SideStore */ = {
isa = PBXGroup;
children = (
@@ -1170,6 +1327,7 @@
B343F84D295F6323002B1159 /* em_proxy.xcodeproj */,
19104DB32909C06D00C49C7B /* EmotionalDamage */,
B343F886295F7F9B002B1159 /* libfragmentzip.xcodeproj */,
+ A8C38C1C2D2068D100E83DBD /* Utils */,
);
path = SideStore;
sourceTree = "";
@@ -1572,12 +1730,12 @@
BF66EEE62501AED0007EE018 /* UIColor+Hex.swift */,
BF66EEE42501AED0007EE018 /* UserDefaults+AltStore.swift */,
BF6A531F246DC1B0004F59C8 /* FileManager+SharedDirectories.swift */,
- 0E0502592BEC83C500879B5C /* OperatingSystemVersion+Comparable.swift */,
0E05025B2BEC947000879B5C /* String+SideStore.swift */,
D5F48B4729CCF21B002B52A4 /* AltStore+Async.swift */,
D5893F7E2A14183200E767CD /* NSManagedObjectContext+Conveniences.swift */,
D52A2F962ACB40F700BDF8E3 /* Logger+AltStore.swift */,
D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */,
+ A8D49F522D3D2F9400844B92 /* ProcessInfo+AltStore.swift */,
D5B6F6A82AD75D01007EED5A /* ProcessInfo+Previews.swift */,
D552EB052AF453F900A3AB4D /* URL+Normalized.swift */,
);
@@ -1619,10 +1777,12 @@
BF98916C250AABF3002ACF50 /* AltWidget */ = {
isa = PBXGroup;
children = (
+ A8AD35562D31BE8F003A28B4 /* Manager */,
+ A80D790B2D2F209700A40F40 /* Intents */,
+ A8096D1D2D30ADD5000C39C6 /* Providers */,
A800F6FE2CE28DE300208744 /* Extensions */,
BF8B17F0250AC62400F8157F /* AltWidgetExtension.entitlements */,
D5FD4EC42A952EAD0097BEE8 /* AltWidgetBundle.swift */,
- D577AB7A2A967DF5007FE952 /* AppsTimelineProvider.swift */,
D50C29F22A8ECD71009AB488 /* Widgets */,
D51AF9752A97D29100471312 /* Model */,
D577AB802A968B7E007FE952 /* Components */,
@@ -1846,7 +2006,6 @@
BFE00A1F2503097F00EB4D0C /* INInteraction+AltStore.swift */,
D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */,
B376FE3D29258C8900E18883 /* OSLog+SideStore.swift */,
- 0E13E5852CC8F55900E9C0DF /* ProcessInfo+SideStore.swift */,
D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */,
D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */,
D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */,
@@ -1858,6 +2017,7 @@
BFDB69FB22A9A7A6007EA6D6 /* Settings */ = {
isa = PBXGroup;
children = (
+ A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */,
BFE60737231ADF49002B0E8E /* Settings.storyboard */,
BFE60739231ADF82002B0E8E /* SettingsViewController.swift */,
0EA426392C2230150026D7FB /* AnisetteServerList.swift */,
@@ -1881,29 +2041,30 @@
children = (
D513F6152A12CE210061EAA1 /* Errors */,
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */,
- BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */,
BF770E5322BC044E002A40FE /* OperationContexts.swift */,
+ BF770E5722BC3D0F002A40FE /* RefreshGroup.swift */,
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */,
+ D561AF812B21669400BF59C6 /* VerifyAppPledgeOperation.swift */,
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
- BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
+ BFCCB519245E3401001853EA /* VerifyAppOperation.swift */,
+ A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */,
+ BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */,
BF3BEFBE2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift */,
- BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */,
+ BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */,
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */,
- BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */,
- BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */,
+ BF3BEFC024086A1E00DE7D55 /* RefreshAppOperation.swift */,
BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */,
- BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */,
- BFCCB519245E3401001853EA /* VerifyAppOperation.swift */,
- BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */,
+ BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */,
+ D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */,
BF3432FA246B894F0052F4A1 /* BackupAppOperation.swift */,
BFDBBD7F246CB84F004ED2F3 /* RemoveAppBackupOperation.swift */,
+ BF44EEFB246B4550002A52F2 /* RemoveAppOperation.swift */,
+ BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */,
BFF00D312501BDA100746320 /* BackgroundRefreshAppsOperation.swift */,
D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */,
D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */,
- D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */,
D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */,
- D561AF812B21669400BF59C6 /* VerifyAppPledgeOperation.swift */,
BF7B44062725A4B8005288A4 /* Patch App */,
);
path = Operations;
@@ -2095,6 +2256,7 @@
D589170128C7D93500E39C8B /* Error Log */ = {
isa = PBXGroup;
children = (
+ A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */,
D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */,
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */,
0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */,
@@ -2556,7 +2718,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "#!/bin/sh\n\necho \"Build directory: $BUILD_DIR\"\necho \"Configuration build directory: $CONFIGURATION_BUILD_DIR\"\n\n# diagnostics\n# echo \">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<\"\n# find \"$BUILD_DIR\" -maxdepth 7 -exec ls -ld {} + || true # List contents if directory exists \n# # ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists \n# echo \"\"\n\n# diagnostics\n# exit 0\n\n# Define the path to your Makefile\nMAKEFILE_PATH=\"${PROJECT_DIR}/\"\n\n# Navigate to the directory containing the Makefile\ncd \"$MAKEFILE_PATH\" || exit 1\n\n# Run the make target 'ipa-altbackup'\nmake -B copy-altbackup ipa-altbackup\n\n# Ensure that the ipa-altbackup process finishes before continuing\nif [ $? -ne 0 ]; then\n echo \"Error: ipa-altbackup failed\"\n exit 1\nelse\n echo \"ipa-altbackup completed successfully\"\nfi\n# Type a script or drag a script file from your workspace to insert its path.\n";
+ shellScript = "#!/bin/sh\n\necho \"Build directory: $BUILD_DIR\"\necho \"Configuration build directory: $CONFIGURATION_BUILD_DIR\"\n\n# diagnostics\n# echo \">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<\"\n# find \"$BUILD_DIR\" -maxdepth 7 -exec ls -ld {} + || true # List contents if directory exists \n# # ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists \n# echo \"\"\n\n# diagnostics\n# exit 0\n\n# Define the path to your Makefile\nMAKEFILE_PATH=\"${PROJECT_DIR}/\"\n\n# Navigate to the directory containing the Makefile\ncd \"$MAKEFILE_PATH\" || exit 1\n\n# Run the make target 'ipa-altbackup'\nmake -B clean-altbackup copy-altbackup ipa-altbackup\n\n# Ensure that the ipa-altbackup process finishes before continuing\nif [ $? -ne 0 ]; then\n echo \"Error: ipa-altbackup failed\"\n exit 1\nelse\n echo \"ipa-altbackup completed successfully\"\nfi\n# Type a script or drag a script file from your workspace to insert its path.\n";
};
AEDB4E9409D2CEE1EA126980 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
@@ -2714,6 +2876,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ A8D49F532D3D2F9400844B92 /* ProcessInfo+AltStore.swift in Sources */,
A82067842D03DC0600645C0D /* OperatingSystemVersion+Comparable.swift in Sources */,
D5FB28EE2ADDF89800A1C337 /* KnownSource.swift in Sources */,
BF66EED32501AECA007EE018 /* AltStore2ToAltStore3.xcmappingmodel in Sources */,
@@ -2823,12 +2986,19 @@
D577AB7F2A96878A007FE952 /* AppDetailWidget.swift in Sources */,
BF98917E250AAC4F002ACF50 /* Countdown.swift in Sources */,
D5151BE22A90363300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */,
+ A8096D1C2D30ADA9000C39C6 /* ActiveAppsTimelineProvider+Simulator.swift in Sources */,
+ A80D790D2D2F20AF00A40F40 /* PaginationIntent.swift in Sources */,
D5FD4EC92A9530C00097BEE8 /* AppSnapshot.swift in Sources */,
+ A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */,
D5151BE72A90395400C96F28 /* View+AltWidget.swift in Sources */,
+ A868CFE42D31999A002F1201 /* SingletonGenericMap.swift in Sources */,
+ A8A853AF2D3065A300995795 /* ActiveAppsTimelineProvider.swift in Sources */,
BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */,
A800F7042CE28E3800208744 /* View+AltWidget.swift in Sources */,
BF98917F250AAC4F002ACF50 /* LockScreenWidget.swift in Sources */,
D5FD4EC52A952EAD0097BEE8 /* AltWidgetBundle.swift in Sources */,
+ A8096D182D30AD4F000C39C6 /* WidgetUpdateIntent.swift in Sources */,
+ A80D790F2D2F217000A40F40 /* PaginationDataHolder.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2838,14 +3008,18 @@
files = (
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
+ A8C38C2C2D206AD900E83DBD /* AbstractClassError.swift in Sources */,
+ A8087E752D2D2958002DB21B /* ImportExport.swift in Sources */,
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */,
D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */,
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
D5151BE12A90344300C96F28 /* RefreshAllAppsWidgetIntent.swift in Sources */,
D513F6162A12CE4E0061EAA1 /* SourceError.swift in Sources */,
+ A8696EE42D34512C00E96389 /* RemoveAppExtensionsOperation.swift in Sources */,
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
BFE6073A231ADF82002B0E8E /* SettingsViewController.swift in Sources */,
+ A86315DF2D3EB2DE0048FA40 /* ErrorProcessing.swift in Sources */,
D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */,
BF8CAE4E248AEABA004D6CCE /* UIDevice+Jailbreak.swift in Sources */,
D5E1E7C128077DE90016FC96 /* UpdateKnownSourcesOperation.swift in Sources */,
@@ -2865,11 +3039,15 @@
BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */,
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
+ A8C38C242D206A3A00E83DBD /* ConsoleLogger.swift in Sources */,
+ A8C38C262D206A3A00E83DBD /* ConsoleLog.swift in Sources */,
D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */,
D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */,
+ A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */,
A8FD917C2D0478D200322782 /* VerificationError.swift in Sources */,
D5A0537329B91DB400997551 /* SourceDetailContentViewController.swift in Sources */,
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
+ A8C38C322D206B2500E83DBD /* FileOutputStream.swift in Sources */,
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */,
@@ -2909,7 +3087,7 @@
BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */,
BF770E5822BC3D0F002A40FE /* RefreshGroup.swift in Sources */,
19B9B7452845E6DF0076EF69 /* SelectTeamViewController.swift in Sources */,
- 0E13E5862CC8F55900E9C0DF /* ProcessInfo+SideStore.swift in Sources */,
+ A88B8C492D35AD3200F53F9D /* OperationsLoggingContolView.swift in Sources */,
D59162AB29BA60A9005CBF47 /* SourceHeaderView.swift in Sources */,
BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */,
BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */,
@@ -2919,6 +3097,7 @@
BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */,
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
+ A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */,
D50107EC2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift in Sources */,
D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */,
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
@@ -2929,15 +3108,18 @@
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
A8FD917B2D0472DD00322782 /* DeprecatedAPIs.swift in Sources */,
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */,
+ A8C38C382D2084D000E83DBD /* ConsoleLogView.swift in Sources */,
BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */,
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
0EE7FDC42BE8BC7900D1E390 /* ALTLocalizedError.swift in Sources */,
BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */,
BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */,
+ A8C38C2A2D206AC100E83DBD /* OutputStream.swift in Sources */,
BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */,
0EE7FDCD2BE9124400D1E390 /* ErrorDetailsViewController.swift in Sources */,
D561AF822B21669400BF59C6 /* VerifyAppPledgeOperation.swift in Sources */,
+ A8B516E32D2666CA0047047C /* CoreDataHelper.swift in Sources */,
BF3432FB246B894F0052F4A1 /* BackupAppOperation.swift in Sources */,
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
diff --git a/AltStore/AltStore.entitlements b/AltStore/AltStore.entitlements
index 27c6a878..02a703df 100644
--- a/AltStore/AltStore.entitlements
+++ b/AltStore/AltStore.entitlements
@@ -2,14 +2,22 @@
+
com.apple.developer.kernel.extended-virtual-addressing
com.apple.developer.kernel.increased-debugging-memory-limit
com.apple.developer.kernel.increased-memory-limit
- aps-environment
- development
+ aps-environment
+ development
com.apple.developer.siri
com.apple.security.application-groups
diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift
index 49c1bda1..b436b686 100644
--- a/AltStore/AppDelegate.swift
+++ b/AltStore/AppDelegate.swift
@@ -41,8 +41,26 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
private let intentHandler = IntentHandler()
private let viewAppIntentHandler = ViewAppIntentHandler()
+ public let consoleLog = ConsoleLog()
+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
+ // navigation bar buttons spacing is too much (so hack it to use minimal spacing)
+ // this is swift-5 specific behavior and might change
+ // https://stackoverflow.com/a/64988363/11971304
+ //
+ // Warning: this affects all screens through out the app, and basically overrides storyboard
+ let stackViewAppearance = UIStackView.appearance(whenContainedInInstancesOf: [UINavigationBar.self])
+ stackViewAppearance.spacing = -8 // adjust as needed
+
+ consoleLog.startCapturing()
+ print("===================================================")
+ print("| App is Starting up |")
+ print("===================================================")
+ print("| Console Logger started capturing output streams |")
+ print("===================================================")
+ print("\n ")
+
// Override point for customization after application launch.
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.MigrationDebug")
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.SQLDebug")
@@ -81,9 +99,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
-// #if DEBUG || BETA
+ #if DEBUG && (targetEnvironment(simulator) || BETA)
UserDefaults.standard.isDebugModeEnabled = true
-// #endif
+ #endif
self.prepareForBackgroundFetch()
@@ -130,6 +148,17 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
default: return nil
}
}
+
+ func applicationWillTerminate(_ application: UIApplication) {
+ // Stop console logging and clean up resources
+ print("\n ")
+ print("===================================================")
+ print("| Console Logger stopped capturing output streams |")
+ print("===================================================")
+ print("| App is being terminated |")
+ print("===================================================")
+ consoleLog.stopCapturing()
+ }
}
extension AppDelegate
@@ -269,7 +298,7 @@ extension AppDelegate
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
}
- #if DEBUG
+ #if DEBUG && targetEnvironment(simulator)
UIApplication.shared.registerForRemoteNotifications()
#endif
}
diff --git a/AltStore/Authentication/AuthenticationViewController.swift b/AltStore/Authentication/AuthenticationViewController.swift
index d1db2e18..3e275e0e 100644
--- a/AltStore/Authentication/AuthenticationViewController.swift
+++ b/AltStore/Authentication/AuthenticationViewController.swift
@@ -125,6 +125,7 @@ private extension AuthenticationViewController
let error = error.withLocalizedTitle(NSLocalizedString("Failed to Log In", comment: ""))
let toastView = ToastView(error: error)
toastView.show(in: self)
+ toastView.backgroundColor = .white
toastView.textLabel.textColor = .altPrimary
toastView.detailTextLabel.textColor = .altPrimary
self.toastView = toastView
diff --git a/AltStore/Components/ToastView.swift b/AltStore/Components/ToastView.swift
index 77c93212..f940b4ac 100644
--- a/AltStore/Components/ToastView.swift
+++ b/AltStore/Components/ToastView.swift
@@ -67,30 +67,10 @@ class ToastView: RSTToastView
convenience init(error: Error)
{
- var error = error as NSError
- var underlyingError = error.underlyingError
+ let error = error as NSError
- if
- let unwrappedUnderlyingError = underlyingError,
- error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
- {
- // Treat underlyingError as the primary error, but keep localized title + failure.
-
- let nsError = error as NSError
- error = unwrappedUnderlyingError as NSError
-
- if let localizedTitle = nsError.localizedTitle {
- error = error.withLocalizedTitle(localizedTitle)
- }
- if let localizedFailure = nsError.localizedFailure {
- error = error.withLocalizedFailure(localizedFailure)
- }
-
- underlyingError = nil
- }
let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
- let detailText = error.localizedDescription
-
+ let detailText = ErrorProcessing(.fullError).getDescription(error: error)
self.init(text: text, detailText: detailText)
}
diff --git a/AltStore/LaunchViewController.swift b/AltStore/LaunchViewController.swift
index 38fae3a3..13c42c80 100644
--- a/AltStore/LaunchViewController.swift
+++ b/AltStore/LaunchViewController.swift
@@ -248,7 +248,9 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
target_minimuxer_address()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do {
- try start(pairing_file, documentsDirectory)
+ // enable minimuxer console logging only if enabled in settings
+ let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
+ try minimuxer.startWithLogger(pairing_file, documentsDirectory, isMinimuxerConsoleLoggingEnabled)
} catch {
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
@@ -311,6 +313,9 @@ extension LaunchViewController
guard case .failure(let error) = result else { return }
Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)")
+ let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError)
+ print("Failed to update sources on launch. \(errorDesc)")
+
let toastView = ToastView(error: error)
toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside)
toastView.show(in: self.destinationViewController.selectedViewController ?? self.destinationViewController)
@@ -318,6 +323,7 @@ extension LaunchViewController
self.updateKnownSources()
+ // Ask widgets to be refreshed
WidgetCenter.shared.reloadAllTimelines()
// Add view controller as child (rather than presenting modally)
diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift
index bea66ab7..39fc0c47 100644
--- a/AltStore/Managing Apps/AppManager.swift
+++ b/AltStore/Managing Apps/AppManager.swift
@@ -626,6 +626,11 @@ extension AppManager
self.fetchSources() { (result) in
do
{
+ // Check if the result is failure and rethrow
+ if case .failure(let error) = result {
+ throw error // Rethrow the error
+ }
+
do
{
let (_, context) = try result.get()
@@ -1146,27 +1151,28 @@ private extension AppManager
case .activate(let app) where UserDefaults.standard.isLegacyDeactivationSupported: fallthrough
case .refresh(let app):
// Check if backup app is installed in place of real app.
- let uti = UTTypeCopyDeclaration(app.installedBackupAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
+// let altBackupUti = UTTypeCopyDeclaration(app.installedBackupAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
- if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
- uti != nil ||
- app.needsResign ||
+// if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
+// altBackupUti != nil || // why would altbackup requires reinstall? it shouldn't cause we are just renewing profiles
+// app.needsResign || // why would an app require resign during refresh? it shouldn't!
// We need to reinstall ourselves on refresh to ensure the new provisioning profile is used
- app.bundleIdentifier == StoreApp.altstoreAppID
- {
+ // => mahee96: jkcoxson confirmed misagent manages profiles independently without requiring lockdownd or installd intervention, so sidestore profile renewal shouldn't require reinstall
+// app.bundleIdentifier == StoreApp.altstoreAppID
+// {
// Resign app instead of just refreshing profiles because either:
- // * Refreshing using different certificate
- // * Backup app is still installed
- // * App explicitly needs resigning
+ // * Refreshing using different certificate // when can this happen?, lets assume, refreshing with different certificate, why not just ask user to re-install manually? (probably we need re-install button)
+ // * Backup app is still installed // but why? I mean the AltBackup was put in place for a reason? ie during refresh just renew appIDs don't care about the app itself.
+ // * App explicitly needs resigning // when can this happen?
// * Device is jailbroken and using AltDaemon on iOS 14.0 or later (b/c refreshing with provisioning profiles is broken)
- let installProgress = self._install(app, operation: operation, group: group) { (result) in
- self.finish(operation, result: result, group: group, progress: progress)
- }
- progress?.addChild(installProgress, withPendingUnitCount: 80)
- }
- else
- {
+// let installProgress = self._install(app, operation: operation, group: group) { (result) in
+// self.finish(operation, result: result, group: group, progress: progress)
+// }
+// progress?.addChild(installProgress, withPendingUnitCount: 80)
+// }
+// else
+// {
// Refreshing with same certificate as last time, and backup app isn't still installed,
// so we can just refresh provisioning profiles.
@@ -1174,7 +1180,7 @@ private extension AppManager
self.finish(operation, result: result, group: group, progress: progress)
}
progress?.addChild(refreshProgress, withPendingUnitCount: 80)
- }
+// }
case .activate(let app):
let activateProgress = self._activate(app, operation: operation, group: group) { (result) in
@@ -1234,132 +1240,6 @@ private extension AppManager
return group
}
- func removeAppExtensions(
- from application: ALTApplication,
- existingApp: InstalledApp?,
- extensions: Set,
- _ presentingViewController: UIViewController?,
- completion: @escaping (Result) -> Void
- ) {
-
- // App-Extensions: Ensure existing app's extensions and currently installing app's extensions must match
- if let existingApp {
- _ = RSTAsyncBlockOperation { _ in
- let existingAppEx: Set = existingApp.appExtensions
- let currentAppEx: Set = application.appExtensions
-
- let currentAppExNames = currentAppEx.map{ appEx in appEx.bundleIdentifier}
- let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier}
-
- let excessExtensions = currentAppEx.filter{
- !(existingAppExNames.contains($0.bundleIdentifier))
- }
-
-
- let isMatching = (currentAppEx.count == existingAppEx.count) && excessExtensions.isEmpty
- let diagnosticsMsg = "AppManager.removeAppExtensions: App Extensions in existingApp and currentApp are matching: \(isMatching)\n"
- + "AppManager.removeAppExtensions: existingAppEx: \(existingAppExNames); currentAppEx: \(String(describing: currentAppExNames))\n"
- print(diagnosticsMsg)
-
-
- // if background mode, then remove only the excess extensions
- guard let presentingViewController: UIViewController = presentingViewController else {
- // perform silent extensions cleanup for those that aren't already present in existing app
- print("\n Performing background mode Extensions removal \n")
- print("AppManager.removeAppExtensions: Excess Extensions: \(excessExtensions)")
-
- do {
- for appExtension in excessExtensions {
- print("Deleting extension \(appExtension.bundleIdentifier)")
- try FileManager.default.removeItem(at: appExtension.fileURL)
- }
- return completion(.success(()))
- } catch {
- return completion(.failure(error))
- }
- }
- }
- }
-
- guard !application.appExtensions.isEmpty else { return completion(.success(())) }
-
- DispatchQueue.main.async {
- let firstSentence: String
-
- if UserDefaults.standard.activeAppLimitIncludesExtensions
- {
- firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "")
- }
- else
- {
- firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
- }
-
- let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "")
-
- let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
- alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
- completion(.failure(OperationError.cancelled))
- }))
- alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
- completion(.success(()))
- })
- alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
- do
- {
- for appExtension in application.appExtensions
- {
- print("Deleting extension \(appExtension.bundleIdentifier)")
- try FileManager.default.removeItem(at: appExtension.fileURL)
- }
-
- completion(.success(()))
- }
- catch
- {
- completion(.failure(error))
- }
- })
-
- if let presentingViewController {
- alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in
- let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in
- do
- {
- for appExtension in selection
- {
- print("Deleting extension \(appExtension.bundleIdentifier)")
-
- try FileManager.default.removeItem(at: appExtension.fileURL)
- }
- completion(.success(()))
- }
- catch
- {
- completion(.failure(error))
- }
- return nil
- }
-
- let suiview = popoverContentController.view!
- suiview.translatesAutoresizingMaskIntoConstraints = false
-
- popoverContentController.modalPresentationStyle = .popover
-
- if let popoverPresentationController = popoverContentController.popoverPresentationController {
- popoverPresentationController.sourceView = presentingViewController.view
- popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4)
- popoverPresentationController.delegate = popoverContentController
-
- presentingViewController.present(popoverContentController, animated: true)
- }
- })
-
- presentingViewController.present(alertController, animated: true)
- }
- }
- }
-
private func _install(_ app: AppProtocol,
operation appOperation: AppOperation,
group: RefreshGroup,
@@ -1469,52 +1349,20 @@ private extension AppManager
verifyOperation.addDependency(downloadOperation)
/* Remove App Extensions */
-
- let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
- do
- {
- if let error = context.error
- {
- throw error
- }
-/*
- guard case .install = appOperation else {
- operation.finish()
- return
- }
-*/
- guard let extensions = context.app?.appExtensions else {
- throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app?.appExtensions is nil")
- }
-
- guard let currentApp = context.app else {
- throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app is nil")
- }
-
-
- self?.removeAppExtensions(from: currentApp,
- existingApp: app as? InstalledApp,
- extensions: extensions,
- context.authenticatedContext.presentingViewController
- ) { result in
- switch result {
- case .success(): break
- case .failure(let error): context.error = error
- }
- operation.finish()
- }
-
- }
- catch
+ let localAppExtensions = (app as? ALTApplication)?.appExtensions
+ let removeAppExtensionsOperation = RemoveAppExtensionsOperation(context: context,
+ localAppExtensions: localAppExtensions)
+ removeAppExtensionsOperation.resultHandler = { (result) in
+ switch result
{
+ case .failure(let error):
context.error = error
- operation.finish()
+ case .success: break
}
}
-
removeAppExtensionsOperation.addDependency(verifyOperation)
-
+
/* Refresh Anisette Data */
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
refreshAnisetteDataOperation.resultHandler = { (result) in
@@ -1529,7 +1377,7 @@ private extension AppManager
/* Fetch Provisioning Profiles */
- let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
+ let fetchProvisioningProfilesOperation = FetchProvisioningProfilesInstallOperation(context: context)
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
fetchProvisioningProfilesOperation.resultHandler = { (result) in
switch result
@@ -1763,7 +1611,7 @@ private extension AppManager
private func exportResginedAppsToDocsDir(_ resignedApp: ALTApplication)
{
// Check if the user has enabled exporting resigned apps to the Documents directory and continue
- guard UserDefaults.standard.isResignedAppExportEnabled else {
+ guard UserDefaults.standard.isExportResignedAppEnabled else {
return
}
@@ -1815,26 +1663,27 @@ private extension AppManager
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
context.app = ALTApplication(fileURL: app.fileURL)
- //App-Extensions: Ensure DB data and disk state must match
- let dbAppEx: Set = Set(app.appExtensions)
- let diskAppEx: Set = Set(context.app!.appExtensions)
- let diskAppExNames = diskAppEx.map { $0.bundleIdentifier }
- let dbAppExNames = dbAppEx.map{ $0.bundleIdentifier }
- let isMatching = Set(dbAppExNames) == Set(diskAppExNames)
+ // Since this doesn't involve modifying app bundle which will cause re-install, this is safe in refresh path
+ //App-Extensions: Ensure DB data and disk state must match
+ let dbAppEx: Set = Set(app.appExtensions)
+ let diskAppEx: Set = Set(context.app!.appExtensions)
+ let diskAppExNames = diskAppEx.map { $0.bundleIdentifier }
+ let dbAppExNames = dbAppEx.map{ $0.bundleIdentifier }
+ let isMatching = Set(dbAppExNames) == Set(diskAppExNames)
- let validateAppExtensionsOperation = RSTAsyncBlockOperation { op in
-
- let errMessage = "AppManager.refresh: App Extensions in DB and Disk are matching: \(isMatching)\n"
- + "AppManager.refresh: dbAppEx: \(dbAppExNames); diskAppEx: \(String(describing: diskAppExNames))\n"
- print(errMessage)
- if(!isMatching){
- completionHandler(.failure(OperationError.refreshAppFailed(message: errMessage)))
- }
- op.finish()
- }
+ let validateAppExtensionsOperation = RSTAsyncBlockOperation { op in
+
+ let errMessage = "AppManager.refresh: App Extensions in DB and Disk are matching: \(isMatching)\n"
+ + "AppManager.refresh: dbAppEx: \(dbAppExNames); diskAppEx: \(String(describing: diskAppExNames))\n"
+ print(errMessage)
+ if(!isMatching){
+ completionHandler(.failure(OperationError.refreshAppFailed(message: errMessage)))
+ }
+ op.finish()
+ }
/* Fetch Provisioning Profiles */
- let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
+ let fetchProvisioningProfilesOperation = FetchProvisioningProfilesRefreshOperation(context: context)
fetchProvisioningProfilesOperation.resultHandler = { (result) in
switch result
{
@@ -1844,7 +1693,7 @@ private extension AppManager
}
}
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 60)
- fetchProvisioningProfilesOperation.addDependency(validateAppExtensionsOperation)
+ // fetchProvisioningProfilesOperation.addDependency(validateAppExtensionsOperation)
/* Refresh */
let refreshAppOperation = RefreshAppOperation(context: context)
@@ -1853,7 +1702,10 @@ private extension AppManager
{
case .success(let installedApp):
completionHandler(.success(installedApp))
-
+
+
+ // refreshing local app's provisioning profile means talking to misagent daemon
+ // which requires loopback vpn
case .failure(MinimuxerError.ProfileInstall):
completionHandler(.failure(OperationError.noWiFi))
@@ -1878,7 +1730,8 @@ private extension AppManager
progress.addChild(refreshAppOperation.progress, withPendingUnitCount: 40)
refreshAppOperation.addDependency(fetchProvisioningProfilesOperation)
- let operations = [validateAppExtensionsOperation, fetchProvisioningProfilesOperation, refreshAppOperation]
+// let operations = [validateAppExtensionsOperation, fetchProvisioningProfilesOperation, refreshAppOperation]
+ let operations = [fetchProvisioningProfilesOperation, refreshAppOperation]
group.add(operations)
self.run(operations, context: group.context)
@@ -2280,6 +2133,7 @@ private extension AppManager
AnalyticsManager.shared.trackEvent(event)
}
+ // Ask widgets to be refreshed
WidgetCenter.shared.reloadAllTimelines()
do
diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift
index 7f7f2948..24dd159c 100644
--- a/AltStore/My Apps/MyAppsViewController.swift
+++ b/AltStore/My Apps/MyAppsViewController.swift
@@ -1151,7 +1151,9 @@ private extension MyAppsViewController
func refresh(_ installedApp: InstalledApp)
{
- guard minimuxerStatus else { return }
+ // we do need minimuxer, coz it needs to talk to misagent daemon which manages profiles
+ // so basically loopback vpn is still required
+ guard minimuxerStatus else { return } // we don't need minimuxer when renewing appIDs only do we, heck we can even do it on mobile internet
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
guard previousProgress == nil else {
@@ -1357,6 +1359,50 @@ private extension MyAppsViewController
self.present(alertController, animated: true, completion: nil)
}
+ func importBackup(for installedApp: InstalledApp){
+ ImportExport.importBackup(presentingViewController: self, for: installedApp) { result in
+ var toast: ToastView
+ switch(result){
+ case .failure(let error):
+ toast = ToastView(error: error, opensLog: false)
+ break
+ case .success:
+ toast = ToastView(text: "Import Backup successful for \(installedApp.name)",
+ detailText: "Use 'Restore Backup' option to restore data from this imported backup")
+ }
+ DispatchQueue.main.async {
+ toast.show(in: self)
+ }
+ }
+ }
+
+ private func getPreviousBackupURL(_ installedApp: InstalledApp) -> URL
+ {
+ let backupURL = FileManager.default.backupDirectoryURL(for: installedApp)!
+ let backupBakURL = ImportExport.getPreviousBackupURL(backupURL)
+ return backupBakURL
+ }
+
+ func restorePreviousBackup(for installedApp: InstalledApp){
+ let backupURL = FileManager.default.backupDirectoryURL(for: installedApp)!
+ let backupBakURL = ImportExport.getPreviousBackupURL(backupURL)
+
+ // backupBakURL is expected to exist at this point, this needs to be ensured by caller logic
+ // or invoke this action only when backupBakURL exists
+
+ // delete the current backup
+ if(FileManager.default.fileExists(atPath: backupURL.path)){
+ try! FileManager.default.removeItem(at: backupURL)
+ }
+
+ // restore the previously saved backup as current backup
+ // (don't delete the N-1 backup yet so copy instead of move)
+ try! FileManager.default.copyItem(at: backupBakURL, to: backupURL)
+
+ //perform restore of data from the backup
+ restore(installedApp)
+ }
+
func restore(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
@@ -1423,7 +1469,10 @@ private extension MyAppsViewController
do
{
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
- tempApp.needsResign = true
+ tempApp.needsResign = true // why do we want to resign it during refresh ?!!!!
+ // I see now, so here we just mark that icon needs to be changed but leave it for refresh/install to do it
+ // this is bad, coz now the weight of installing goes to refresh step !!! which is not what we want
+
tempApp.hasAlternateIcon = (image != nil)
if let image = image
@@ -1459,27 +1508,27 @@ private extension MyAppsViewController
}
}
- func enableJIT(for installedApp: InstalledApp)
- {
-
+ func enableJIT(for installedApp: InstalledApp) {
let sidejitenabled = UserDefaults.standard.sidejitenable
- if #unavailable(iOS 17) {
+ if #unavailable(iOS 17), !sidejitenabled {
guard minimuxerStatus else { return }
}
-
if #available(iOS 17, *), !sidejitenabled {
- ToastView(error: (OperationError.tooNewError as NSError).withLocalizedTitle("No iOS 17 On Device JIT!"), opensLog: true).show(in: self)
- AppManager.shared.log(OperationError.tooNewError, operation: .enableJIT, app: installedApp)
+ let error = OperationError.tooNewError as NSError
+ let localizedError = error.withLocalizedTitle("No iOS 17 On Device JIT!")
+
+ ToastView(error: localizedError, opensLog: true).show(in: self)
+ AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
return
}
AppManager.shared.enableJIT(for: installedApp) { result in
DispatchQueue.main.async {
- switch result
- {
- case .success: break
+ switch result {
+ case .success:
+ break
case .failure(let error):
ToastView(error: error, opensLog: true).show(in: self)
AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
@@ -1810,9 +1859,17 @@ extension MyAppsViewController
self.exportBackup(for: installedApp)
}
- let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in
+ let importBackupAction = UIAction(title: NSLocalizedString("Import Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in
+ self.importBackup(for: installedApp)
+ }
+
+ let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: "Restores the last or current backup of this app"), image: UIImage(systemName: "arrow.down.doc")) { (action) in
self.restore(installedApp)
}
+
+ let restorePreviousBackupAction = UIAction(title: NSLocalizedString("Restore Previous Backup", comment: "Restores the backup saved before the current backup was created."), image: UIImage(systemName: "arrow.down.doc")) { (action) in
+ self.restorePreviousBackup(for: installedApp)
+ }
let chooseIconAction = UIAction(title: NSLocalizedString("Photos", comment: ""), image: UIImage(systemName: "photo")) { (action) in
self.chooseIcon(for: installedApp)
@@ -1878,7 +1935,8 @@ extension MyAppsViewController
var outError: NSError? = nil
self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in
- #if DEBUG
+
+ #if DEBUG && targetEnvironment(simulator)
backupExists = true
#else
backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path)
@@ -1903,15 +1961,21 @@ extension MyAppsViewController
if installedApp.isActive
{
actions.append(deactivateAction)
+ // import backup into shared backups dir is allowed
+ actions.append(importBackupAction)
}
- #if DEBUG
+ // have an option to restore the n-1 backup
+ if FileManager.default.fileExists(atPath: getPreviousBackupURL(installedApp).path){
+ actions.append(restorePreviousBackupAction)
+ }
+
+ #if DEBUG && targetEnvironment(simulator)
if installedApp.bundleIdentifier != StoreApp.altstoreAppID
{
actions.append(removeAction)
}
-
#else
if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier)
@@ -1929,6 +1993,26 @@ extension MyAppsViewController
#endif
}
+ // Change the order of entries to make changes to how the context menu is displayed
+ let orderedActions = [
+ openMenu,
+ refreshAction,
+ activateAction,
+ jitAction,
+ changeIconMenu,
+ backupAction,
+ exportBackupAction,
+ importBackupAction,
+ restoreBackupAction,
+ restorePreviousBackupAction,
+ deactivateAction,
+ removeAction,
+ ]
+
+ // remove non-selected actions from the all-actions ordered list
+ // this way the declaration of the action in the above code doesn't determine the context menu order
+ actions = orderedActions.filter{ action in actions.contains(action)}
+
var title: String?
if let storeApp = installedApp.storeApp, storeApp.isPledgeRequired, !storeApp.isPledged
diff --git a/AltStore/Operations/AuthenticationOperation.swift b/AltStore/Operations/AuthenticationOperation.swift
index 730edd11..3e8fa711 100644
--- a/AltStore/Operations/AuthenticationOperation.swift
+++ b/AltStore/Operations/AuthenticationOperation.swift
@@ -715,9 +715,10 @@ private extension AuthenticationOperation
// If we're not using the same certificate used to install AltStore, warn user that they need to refresh.
guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) }
-#if DEBUG
- completionHandler(false)
-#else
+// #if DEBUG && targetEnvironment(simulator)
+// completionHandler(false)
+// #else
+
DispatchQueue.main.async {
let context = AuthenticatedOperationContext(context: self.context)
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
@@ -733,7 +734,7 @@ private extension AuthenticationOperation
completionHandler(false)
}
}
-#endif
+// #endif
}
}
diff --git a/AltStore/Operations/BackgroundRefreshAppsOperation.swift b/AltStore/Operations/BackgroundRefreshAppsOperation.swift
index 47c6c1ea..0d6cbde1 100644
--- a/AltStore/Operations/BackgroundRefreshAppsOperation.swift
+++ b/AltStore/Operations/BackgroundRefreshAppsOperation.swift
@@ -103,7 +103,14 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result: ResultOperation
guard let installedApp = self.context.installedApp else {
return self.finish(.failure(OperationError.invalidParameters("EnableJITOperation.main: self.context.installedApp is nil")))
}
- if #available(iOS 17, *) {
- let sideJITenabled = UserDefaults.standard.sidejitenable
- let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? ""
-
- if sideJITenabled {
- installedApp.managedObjectContext?.perform {
- EnableJITSideJITServer(serverurl: SideJITIP, installedapp: installedApp) { result in
- switch result {
- case .failure(let error):
- switch error {
- case .invalidURL, .errorConnecting:
- self.finish(.failure(OperationError.unableToConnectSideJIT))
- case .deviceNotFound:
- self.finish(.failure(OperationError.unableToRespondSideJITDevice))
- case .other(let message):
- if let startRange = message.range(of: ""),
- let endRange = message.range(of: "
", range: startRange.upperBound.."),
+ let endRange = message.range(of: "
", range: startRange.upperBound..: ResultOperation
}
@available(iOS 17, *)
-func EnableJITSideJITServer(serverurl: String, installedapp: InstalledApp, completion: @escaping (Result) -> Void) {
+func enableJITSideJITServer(serverURL: URL, installedApp: InstalledApp, completion: @escaping (Result) -> Void) {
guard let udid = fetch_udid()?.toString() else {
completion(.failure(.other("Unable to get UDID")))
return
}
- var SJSURL = serverurl
+ let serverURLWithUDID = serverURL.appendingPathComponent(udid)
+ let fullURL = serverURLWithUDID.appendingPathComponent(installedApp.resignedBundleIdentifier)
- if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
- SJSURL = "http://sidejitserver._http._tcp.local:8080"
- }
-
- if !SJSURL.hasPrefix("http") {
- completion(.failure(.invalidURL))
- return
- }
-
- let fullurl = SJSURL + "/\(udid)/" + installedapp.resignedBundleIdentifier
-
- let url = URL(string: fullurl)!
-
- let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
+ let task = URLSession.shared.dataTask(with: fullURL) { (data, response, error) in
if let error = error {
completion(.failure(.errorConnecting))
return
}
- guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return }
+ guard let data = data, let dataString = String(data: data, encoding: .utf8) else {
+ return
+ }
- if datastring == "Enabled JIT for '\(installedapp.name)'!" {
+ if dataString == "Enabled JIT for '\(installedApp.name)'!" {
let content = UNMutableNotificationContent()
content.title = "JIT Successfully Enabled"
- content.subtitle = "JIT Enabled For \(installedapp.name)"
- content.sound = UNNotificationSound.default
-
+ content.subtitle = "JIT Enabled For \(installedApp.name)"
+ content.sound = .default
+
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
let request = UNNotificationRequest(identifier: "EnabledJIT", content: content, trigger: nil)
-
UNUserNotificationCenter.current().add(request)
+
completion(.success(()))
} else {
- let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring)
+ let errorType: SideJITServerErrorType = dataString == "Could not find device!"
+ ? .deviceNotFound
+ : .other(dataString)
completion(.failure(errorType))
}
}
diff --git a/AltStore/Operations/Errors/OperationError.swift b/AltStore/Operations/Errors/OperationError.swift
index 775ba34a..52486a1b 100644
--- a/AltStore/Operations/Errors/OperationError.swift
+++ b/AltStore/Operations/Errors/OperationError.swift
@@ -58,6 +58,8 @@ extension OperationError
case anisetteV3Error//(message: String)
case cacheClearError//(errors: [String])
case noWiFi
+
+ case invalidOperationContext
}
static var cancelled: CancellationError { CancellationError() }
@@ -130,6 +132,10 @@ extension OperationError
OperationError(code: .invalidParameters, failureReason: message)
}
+ static func invalidOperationContext(_ message: String? = nil) -> OperationError {
+ OperationError(code: .invalidOperationContext, failureReason: message)
+ }
+
static func forbidden(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line)
}
@@ -232,7 +238,10 @@ struct OperationError: ALTLocalizedError {
case .invalidParameters:
let message = self._failureReason.map { ": \n\($0)" } ?? "."
- return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message)
+ return String(format: NSLocalizedString("Invalid parameters\n%@", comment: ""), message)
+ case .invalidOperationContext:
+ let message = self._failureReason.map { ": \n\($0)" } ?? "."
+ return String(format: NSLocalizedString("Invalid Operation Context\n%@", comment: ""), message)
case .serverNotFound: return NSLocalizedString("AltServer could not be found.", comment: "")
case .connectionFailed: return NSLocalizedString("A connection to AltServer could not be established.", comment: "")
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
diff --git a/AltStore/Operations/FetchAnisetteDataOperation.swift b/AltStore/Operations/FetchAnisetteDataOperation.swift
index 37d25eb1..93e10383 100644
--- a/AltStore/Operations/FetchAnisetteDataOperation.swift
+++ b/AltStore/Operations/FetchAnisetteDataOperation.swift
@@ -14,6 +14,8 @@ import AltStoreCore
import AltSign
import Roxas
+class ANISETTE_VERBOSITY: Operation {} // dummy tag iface
+
@objc(FetchAnisetteDataOperation)
final class FetchAnisetteDataOperation: ResultOperation, WebSocketDelegate
{
@@ -58,7 +60,7 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
UserDefaults.standard.menuAnisetteURL = urlString
let url = URL(string: urlString)
self.url = url
- print("Anisette URL: \(self.url!.absoluteString)")
+ self.printOut("Anisette URL: \(self.url!.absoluteString)")
if let identifier = Keychain.shared.identifier,
let adiPb = Keychain.shared.adiPb {
@@ -107,7 +109,7 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
guard let url = URL(string: currentServerUrlString) else {
// Invalid URL, skip to next
let errmsg = "Skipping invalid URL: \(currentServerUrlString)"
- print(errmsg)
+ self.printOut(errmsg)
showToast(viewContext: viewContext, message: errmsg)
tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
return
@@ -118,7 +120,7 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
if success {
// If the server is reachable, return the URL
let okmsg = "Found working server: \(url.absoluteString)"
- print(okmsg)
+ self.printOut(okmsg)
if(currentIndex > 0){
// notify user if available server is different the user-specified one
self.showToast(viewContext: viewContext, message: okmsg)
@@ -127,7 +129,7 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
} else {
// If not, try the next URL
let errmsg = "Failed to reach server: \(url.absoluteString), trying next server."
- print(errmsg)
+ self.printOut(errmsg)
self.showToast(viewContext: viewContext, message: errmsg)
self.tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
}
@@ -170,10 +172,10 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
if v3 {
if json["result"] == "GetHeadersError" {
let message = json["message"]
- print("Error getting V3 headers: \(message ?? "no message")")
+ self.printOut("Error getting V3 headers: \(message ?? "no message")")
if let message = message,
message.contains("-45061") {
- print("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
+ self.printOut("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
Keychain.shared.adiPb = nil
return provision()
} else { throw OperationError.anisetteV3Error(message: message ?? "Unknown error") }
@@ -214,16 +216,16 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
if let response = response,
let version = response.value(forHTTPHeaderField: "Implementation-Version") {
- print("Implementation-Version: \(version)")
- } else { print("No Implementation-Version header") }
+ self.printOut("Implementation-Version: \(version)")
+ } else { self.printOut("No Implementation-Version header") }
- print("Anisette used: \(formattedJSON)")
- print("Original JSON: \(json)")
+ self.printOut("Anisette used: \(formattedJSON)")
+ self.printOut("Original JSON: \(json)")
if let anisette = ALTAnisetteData(json: formattedJSON) {
- print("Anisette is valid!")
+ self.printOut("Anisette is valid!")
self.finish(.success(anisette))
} else {
- print("Anisette is invalid!!!!")
+ self.printOut("Anisette is invalid!!!!")
if v3 {
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not have all the required fields)")
} else {
@@ -242,22 +244,22 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
// MARK: - V1
func handleV1() {
- print("Server is V1")
+ self.printOut("Server is V1")
if UserDefaults.shared.trustedServerURL == AnisetteManager.currentURLString {
- print("Server has already been trusted, fetching anisette")
+ self.printOut("Server has already been trusted, fetching anisette")
return self.fetchAnisetteV1()
}
- print("Alerting user about outdated server")
+ self.printOut("Alerting user about outdated server")
let alert = UIAlertController(title: "WARNING: Outdated anisette server", message: "We've detected you are using an older anisette server. Using this server has a higher likelihood of locking your account and causing other issues. Are you sure you want to continue?", preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Continue", style: UIAlertAction.Style.destructive, handler: { action in
- print("Fetching anisette via V1")
+ self.printOut("Fetching anisette via V1")
UserDefaults.shared.trustedServerURL = AnisetteManager.currentURLString
self.fetchAnisetteV1()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: { action in
- print("Cancelled anisette operation")
+ self.printOut("Cancelled anisette operation")
self.finish(.failure(OperationError.cancelled))
}))
@@ -273,14 +275,14 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
}
func fetchAnisetteV1() {
- print("Fetching anisette V1")
+ self.printOut("Fetching anisette V1")
URLSession.shared.dataTask(with: self.url!) { data, response, error in
do {
guard let data = data, error == nil else { throw OperationError.anisetteV1Error(message: "Unable to fetch data\(error != nil ? " (\(error!.localizedDescription))" : "")") }
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: false)
} catch let error as NSError {
- print("Failed to load: \(error.localizedDescription)")
+ self.printOut("Failed to load: \(error.localizedDescription)")
self.finish(.failure(error))
}
}.resume()
@@ -290,7 +292,7 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
func provision() {
fetchClientInfo {
- print("Getting provisioning URLs")
+ self.printOut("Getting provisioning URLs")
var request = self.buildAppleRequest(url: URL(string: "https://gsa.apple.com/grandslam/GsService2/lookup")!)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) { data, response, error in
@@ -302,12 +304,12 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
let endProvisioningURL = URL(string: endProvisioningString) {
self.startProvisioningURL = startProvisioningURL
self.endProvisioningURL = endProvisioningURL
- print("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
- print("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
- print("Starting a provisioning session")
+ self.printOut("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
+ self.printOut("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
+ self.printOut("Starting a provisioning session")
self.startProvisioningSession()
} else {
- print("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
+ self.printOut("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid URLs. Please try again later", message: nil)))
}
}.resume()
@@ -329,19 +331,19 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
do {
if let json = try JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: []) as? [String: Any] {
guard let result = json["result"] as? String else {
- print("The server didn't give us a result")
+ self.printOut("The server didn't give us a result")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a result", message: nil)))
return
}
- print("Received result: \(result)")
+ self.printOut("Received result: \(result)")
switch result {
case "GiveIdentifier":
- print("Giving identifier")
+ self.printOut("Giving identifier")
client.json(["identifier": Keychain.shared.identifier!])
case "GiveStartProvisioningData":
- print("Getting start provisioning data")
+ self.printOut("Getting start provisioning data")
let body = [
"Header": [String: Any](),
"Request": [String: Any](),
@@ -353,19 +355,19 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
if let data = data,
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary>,
let spim = plist["Response"]?["spim"] as? String {
- print("Giving start provisioning data")
+ self.printOut("Giving start provisioning data")
client.json(["spim": spim])
} else {
- print("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
+ self.printOut("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid start provisioning data. Please try again later", message: nil)))
}
}.resume()
case "GiveEndProvisioningData":
- print("Getting end provisioning data")
+ self.printOut("Getting end provisioning data")
guard let cpim = json["cpim"] as? String else {
- print("The server didn't give us a cpim")
+ self.printOut("The server didn't give us a cpim")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a cpim", message: nil)))
return
@@ -384,20 +386,20 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary>,
let ptm = plist["Response"]?["ptm"] as? String,
let tk = plist["Response"]?["tk"] as? String {
- print("Giving end provisioning data")
+ self.printOut("Giving end provisioning data")
client.json(["ptm": ptm, "tk": tk])
} else {
- print("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
+ self.printOut("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid end provisioning data. Please try again later", message: nil)))
}
}.resume()
case "ProvisioningSuccess":
- print("Provisioning succeeded!")
+ self.printOut("Provisioning succeeded!")
client.disconnect(closeCode: 0)
guard let adiPb = json["adi_pb"] as? String else {
- print("The server didn't give us an adi.pb file")
+ self.printOut("The server didn't give us an adi.pb file")
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us an adi.pb file", message: nil)))
return
}
@@ -406,27 +408,27 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
default:
if result.contains("Error") || result.contains("Invalid") || result == "ClosingPerRequest" || result == "Timeout" || result == "TextOnly" {
- print("Failing because of \(result)")
+ self.printOut("Failing because of \(result)")
self.finish(.failure(OperationError.provisioningError(result: result, message: json["message"] as? String)))
}
}
}
} catch let error as NSError {
- print("Failed to handle text: \(error.localizedDescription)")
+ self.printOut("Failed to handle text: \(error.localizedDescription)")
self.finish(.failure(OperationError.provisioningError(result: error.localizedDescription, message: nil)))
}
case .connected:
- print("Connected")
+ self.printOut("Connected")
case .disconnected(let string, let code):
- print("Disconnected: \(code); \(string)")
+ self.printOut("Disconnected: \(code); \(string)")
case .error(let error):
- print("Got error: \(String(describing: error))")
+ self.printOut("Got error: \(String(describing: error))")
default:
- print("Unknown event: \(event)")
+ self.printOut("Unknown event: \(event)")
}
}
@@ -460,10 +462,10 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
self.mdLu != nil &&
self.deviceId != nil &&
Keychain.shared.identifier != nil {
- print("Skipping client_info fetch since all the properties we need aren't nil")
+ self.printOut("Skipping client_info fetch since all the properties we need aren't nil")
return callback()
}
- print("Trying to get client_info")
+ self.printOut("Trying to get client_info")
let clientInfoURL = self.url!.appendingPathComponent("v3").appendingPathComponent("client_info")
URLSession.shared.dataTask(with: clientInfoURL) { data, response, error in
do {
@@ -473,20 +475,20 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
if let clientInfo = json["client_info"] {
- print("Server is V3")
+ self.printOut("Server is V3")
self.clientInfo = clientInfo
self.userAgent = json["user_agent"]!
- print("Client-Info: \(self.clientInfo!)")
- print("User-Agent: \(self.userAgent!)")
+ self.printOut("Client-Info: \(self.clientInfo!)")
+ self.printOut("User-Agent: \(self.userAgent!)")
if Keychain.shared.identifier == nil {
- print("Generating identifier")
+ self.printOut("Generating identifier")
var bytes = [Int8](repeating: 0, count: 16)
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
if status != errSecSuccess {
- print("ERROR GENERATING IDENTIFIER!!! \(status)")
+ self.printOut("ERROR GENERATING IDENTIFIER!!! \(status)")
return self.finish(.failure(OperationError.provisioningError(result: "Couldn't generate identifier", message: nil)))
}
@@ -495,16 +497,16 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
let decoded = Data(base64Encoded: Keychain.shared.identifier!)!
self.mdLu = decoded.sha256().hexEncodedString()
- print("X-Apple-I-MD-LU: \(self.mdLu!)")
+ self.printOut("X-Apple-I-MD-LU: \(self.mdLu!)")
let uuid: UUID = decoded.object()
self.deviceId = uuid.uuidString.uppercased()
- print("X-Mme-Device-Id: \(self.deviceId!)")
+ self.printOut("X-Mme-Device-Id: \(self.deviceId!)")
callback()
} else { self.handleV1() }
} else { self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The returned data may not be in JSON"))) }
} catch let error as NSError {
- print("Failed to load: \(error.localizedDescription)")
+ self.printOut("Failed to load: \(error.localizedDescription)")
self.handleV1()
}
}.resume()
@@ -512,7 +514,7 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
fetchClientInfo {
- print("Fetching anisette V3")
+ self.printOut("Fetching anisette V3")
let url = UserDefaults.standard.menuAnisetteURL
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
request.httpMethod = "POST"
@@ -527,12 +529,21 @@ final class FetchAnisetteDataOperation: ResultOperation, WebSoc
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: true)
} catch let error as NSError {
- print("Failed to load: \(error.localizedDescription)")
+ self.printOut("Failed to load: \(error.localizedDescription)")
self.finish(.failure(error))
}
}.resume()
}
}
+
+
+ private func printOut(_ text: String?){
+ let isInternalLoggingEnabled = OperationsLoggingControl.getFromDatabase(for: ANISETTE_VERBOSITY.self)
+ if(isInternalLoggingEnabled){
+ // logging enabled, so log it
+ text.map{ _ in print(text!) } ?? print()
+ }
+ }
}
extension WebSocketClient {
diff --git a/AltStore/Operations/FetchProvisioningProfilesOperation.swift b/AltStore/Operations/FetchProvisioningProfilesOperation.swift
index 990914c2..096110c2 100644
--- a/AltStore/Operations/FetchProvisioningProfilesOperation.swift
+++ b/AltStore/Operations/FetchProvisioningProfilesOperation.swift
@@ -13,15 +13,16 @@ import AltSign
import Roxas
@objc(FetchProvisioningProfilesOperation)
-final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
+class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
{
let context: AppOperationContext
var additionalEntitlements: [ALTEntitlement: Any]?
- private let appGroupsLock = NSLock()
+ internal let appGroupsLock = NSLock()
- init(context: AppOperationContext)
+ // this class is abstract or shouldn't be instantiated outside, use the subclasses
+ fileprivate init(context: AppOperationContext)
{
self.context = context
@@ -40,11 +41,13 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
return
}
- guard
- let team = self.context.team,
- let session = self.context.session
- else {
- return self.finish(.failure(OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil"))) }
+ guard let team = self.context.team,
+ let session = self.context.session else {
+
+ return self.finish(.failure(
+ OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil"))
+ )
+ }
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) }
@@ -120,7 +123,11 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
extension FetchProvisioningProfilesOperation
{
- func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void)
+ private func prepareProvisioningProfile(for app: ALTApplication,
+ parentApp: ALTApplication?,
+ team: ALTTeam,
+ session: ALTAppleAPISession, c
+ completionHandler: @escaping (Result) -> Void)
{
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
@@ -134,19 +141,21 @@ extension FetchProvisioningProfilesOperation
// or if installedApp.team is nil but resignedBundleIdentifier contains the team's identifier.
let teamsMatch = installedApp.team?.identifier == team.identifier || (installedApp.team == nil && installedApp.resignedBundleIdentifier.contains(team.identifier))
-// #if DEBUG
-//
-// if app.isAltStoreApp
-// {
-// // Use legacy bundle ID format for AltStore.
-// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
-// }
-// else
-// {
-// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
-// }
-//
-// #else
+ // TODO: @mahee96: Try to keep the debug build and release build operations similar, refactor later with proper reasoning
+ // for now, restricted it to debug on simulator only
+ #if DEBUG && targetEnvironment(simulator)
+
+ if app.isAltStoreApp
+ {
+ // Use legacy bundle ID format for AltStore.
+ preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
+ }
+ else
+ {
+ preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
+ }
+
+ #else
if teamsMatch
{
@@ -160,7 +169,7 @@ extension FetchProvisioningProfilesOperation
preferredBundleID = nil
}
- // #endif
+ #endif
}
else
{
@@ -211,35 +220,22 @@ extension FetchProvisioningProfilesOperation
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
-
- // Update features
- self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
- switch result
- {
- case .failure(let error): completionHandler(.failure(error))
- case .success(let appID):
-
- // Update app groups
- self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
- switch result
- {
- case .failure(let error): completionHandler(.failure(error))
- case .success(let appID):
-
- // Fetch Provisioning Profile
- self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
- completionHandler(result)
- }
- }
- }
- }
- }
+
+ //process
+ self.fetchProvisioningProfile(
+ for: appID, team: team, session: session, completionHandler: completionHandler
+ )
}
}
}
}
- func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void)
+ private func registerAppID(for application: ALTApplication,
+ name: String,
+ bundleIdentifier: String,
+ team: ALTTeam,
+ session: ALTAppleAPISession,
+ completionHandler: @escaping (Result) -> Void)
{
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
do
@@ -333,7 +329,81 @@ extension FetchProvisioningProfilesOperation
}
}
- func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void)
+ internal func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void)
+ {
+ ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
+ switch Result(profile, error)
+ {
+ case .failure(let error): completionHandler(.failure(error))
+ case .success(let profile):
+
+ // Delete existing profile
+ ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
+ switch Result(success, error)
+ {
+ case .failure:
+ // As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
+ // So instead, we just return the fetched profile from above.
+ completionHandler(.success(profile))
+
+ case .success:
+ Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).")
+
+ // Fetch new provisioning profile
+ ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
+ completionHandler(Result(profile, error))
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesOperation, @unchecked Sendable {
+ override init(context: AppOperationContext)
+ {
+ super.init(context: context)
+ }
+}
+
+class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperation, @unchecked Sendable{
+ override init(context: AppOperationContext)
+ {
+ super.init(context: context)
+ }
+
+ // modify Operations are allowed for the app groups and other stuffs
+ func fetchProvisioningProfile(appID: ALTAppID,
+ for app: ALTApplication,
+ team: ALTTeam,
+ session: ALTAppleAPISession,
+ completionHandler: @escaping (Result) -> Void)
+ {
+
+ // Update features
+ self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
+ switch result
+ {
+ case .failure(let error): completionHandler(.failure(error))
+ case .success(let appID):
+
+ // Update app groups
+ self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
+ switch result
+ {
+ case .failure(let error): completionHandler(.failure(error))
+ case .success(let appID):
+
+ // Fetch Provisioning Profile
+ super.fetchProvisioningProfile(for: appID, team: team, session: session, completionHandler: completionHandler)
+ }
+ }
+ }
+ }
+ }
+
+ private func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void)
{
var entitlements = app.entitlements
for (key, value) in additionalEntitlements ?? [:]
@@ -412,7 +482,7 @@ extension FetchProvisioningProfilesOperation
}
}
- func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void)
+ private func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void)
{
var entitlements = app.entitlements
for (key, value) in additionalEntitlements ?? [:]
@@ -511,7 +581,7 @@ extension FetchProvisioningProfilesOperation
Logger.sideload.notice("Created new App Group \(group.groupIdentifier, privacy: .public).")
groups.append(group)
- case .failure(let error):
+ case .failure(let error):
Logger.sideload.notice("Failed to create new App Group \(adjustedGroupIdentifier, privacy: .public). \(error.localizedDescription, privacy: .public)")
errors.append(error)
}
@@ -547,34 +617,4 @@ extension FetchProvisioningProfilesOperation
}
}
}
-
- func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void)
- {
- ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
- switch Result(profile, error)
- {
- case .failure(let error): completionHandler(.failure(error))
- case .success(let profile):
-
- // Delete existing profile
- ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
- switch Result(success, error)
- {
- case .failure:
- // As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
- // So instead, we just return the fetched profile from above.
- completionHandler(.success(profile))
-
- case .success:
- Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).")
-
- // Fetch new provisioning profile
- ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
- completionHandler(Result(profile, error))
- }
- }
- }
- }
- }
- }
}
diff --git a/AltStore/Operations/Operation.swift b/AltStore/Operations/Operation.swift
index 0bf322dc..4ba9c94f 100644
--- a/AltStore/Operations/Operation.swift
+++ b/AltStore/Operations/Operation.swift
@@ -38,10 +38,14 @@ class ResultOperation: Operation
result = .failure(error)
}
- // diagnostics logging
- let resultStatus = String(describing: result).prefix("success".count).uppercased()
- print("\n ====> OPERATION: `\(type(of: self))` completed with: \(resultStatus) <====\n\n" +
- " Result: \(result)\n")
+ // Diagnostics: perform verbose logging of the operations only if enabled (so as to not flood console logs)
+ let isLoggingEnabledForThisOperation = OperationsLoggingControl.getFromDatabase(for: type(of: self))
+ if UserDefaults.standard.isVerboseOperationsLoggingEnabled && isLoggingEnabledForThisOperation {
+ // diagnostics logging
+ let resultStatus = String(describing: result).prefix("success".count).uppercased()
+ print("\n ====> OPERATION: `\(type(of: self))` completed with: \(resultStatus) <====\n\n" +
+ " Result: \(result)\n")
+ }
self.resultHandler?(result)
diff --git a/AltStore/Operations/RemoveAppBackupOperation.swift b/AltStore/Operations/RemoveAppBackupOperation.swift
index 45827c01..113b130a 100644
--- a/AltStore/Operations/RemoveAppBackupOperation.swift
+++ b/AltStore/Operations/RemoveAppBackupOperation.swift
@@ -58,17 +58,18 @@ final class RemoveAppBackupOperation: ResultOperation
}
catch let error as CocoaError where error.code == CocoaError.Code.fileNoSuchFile
{
- #if DEBUG
-
- // When debugging, it's expected that app groups don't match, so ignore.
- self.finish(.success(()))
-
- #else
+ // TODO: @mahee96: Find out why should in debug builds the app-groups is not expected to match
+// #if DEBUG
+//
+// // When debugging, it's expected that app groups don't match, so ignore.
+// self.finish(.success(()))
+//
+// #else
Logger.sideload.error("Failed to remove app backup directory \(backupDirectoryURL.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
- #endif
+// #endif
}
catch
{
diff --git a/AltStore/Operations/RemoveAppExtensionsOperation.swift b/AltStore/Operations/RemoveAppExtensionsOperation.swift
new file mode 100644
index 00000000..da12500a
--- /dev/null
+++ b/AltStore/Operations/RemoveAppExtensionsOperation.swift
@@ -0,0 +1,219 @@
+//
+// RefreshAppOperation.swift
+// AltStore
+//
+// Created by Riley Testut on 2/27/20.
+// Copyright © 2020 Riley Testut. All rights reserved.
+//
+
+import Foundation
+
+import AltStoreCore
+import Roxas
+import AltSign
+
+@objc(RemoveAppExtensionsOperation)
+final class RemoveAppExtensionsOperation: ResultOperation
+{
+ let context: AppOperationContext
+ let localAppExtensions: Set?
+
+ init(context: AppOperationContext, localAppExtensions: Set?)
+ {
+ self.context = context
+ self.localAppExtensions = localAppExtensions
+ super.init()
+ }
+
+ override func main()
+ {
+ super.main()
+
+ if let error = self.context.error
+ {
+ self.finish(.failure(error))
+ return
+ }
+
+ guard let targetAppBundle = context.app else {
+ return self.finish(.failure(
+ OperationError.invalidParameters("RemoveAppExtensionsOperation: context.app is nil")
+ ))
+ }
+
+ self.removeAppExtensions(from: targetAppBundle,
+ localAppExtensions: localAppExtensions,
+ extensions: targetAppBundle.appExtensions,
+ context.authenticatedContext.presentingViewController)
+
+ }
+
+
+ private static func removeExtensions(from extensions: Set) throws {
+ for appExtension in extensions {
+ print("Deleting extension \(appExtension.bundleIdentifier)")
+ try FileManager.default.removeItem(at: appExtension.fileURL)
+ }
+ }
+
+
+
+ private func removeAppExtensions(from targetAppBundle: ALTApplication,
+ localAppExtensions: Set?,
+ extensions: Set,
+ _ presentingViewController: UIViewController?)
+ {
+
+ // target App Bundle doesn't contain extensions so don't bother
+ guard !targetAppBundle.appExtensions.isEmpty else {
+ return self.finish(.success(()))
+ }
+
+ // process extensionsInfo
+ let excessExtensions = processExtensionsInfo(from: targetAppBundle, localAppExtensions: localAppExtensions)
+
+ DispatchQueue.main.async {
+ guard let presentingViewController: UIViewController = presentingViewController,
+ presentingViewController.viewIfLoaded?.window != nil else {
+ // background mode: remove only the excess extensions automatically for re-installs
+ // keep all extensions for fresh install (localAppBundle = nil)
+ return self.backgroundModeExtensionsCleanup(excessExtensions: excessExtensions)
+ }
+
+ // present prompt to the user if we have a view context
+ let alertController = self.createAlertDialog(from: targetAppBundle, extensions: extensions, presentingViewController)
+ presentingViewController.present(alertController, animated: true){
+
+ // if for any reason the view wasn't presented, then just signal that as error
+ if presentingViewController.presentedViewController == nil {
+ let errMsg = "RemoveAppExtensionsOperation: unable to present dialog, view context not available." +
+ "\nDid you move to different screen or background after starting the operation?"
+ self.finish(.failure(
+ OperationError.invalidOperationContext(errMsg)
+ ))
+ }
+ }
+ }
+ }
+
+ private func createAlertDialog(from targetAppBundle: ALTApplication,
+ extensions: Set,
+ _ presentingViewController: UIViewController) -> UIAlertController
+ {
+
+ /// Foreground prompt:
+ let firstSentence: String
+
+ if UserDefaults.standard.activeAppLimitIncludesExtensions
+ {
+ firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "")
+ }
+ else
+ {
+ firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
+ }
+
+ let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "")
+
+
+
+ let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
+ alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
+ self.finish(.failure(OperationError.cancelled))
+ }))
+ alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
+ self.finish(.success(()))
+ })
+ alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
+ do {
+ try Self.removeExtensions(from: targetAppBundle.appExtensions)
+ return self.finish(.success(()))
+ } catch {
+ return self.finish(.failure(error))
+ }
+ })
+
+
+
+ alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in
+
+
+ let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in
+ do {
+ try Self.removeExtensions(from: Set(selection))
+ return self.finish(.success(()))
+ } catch {
+ return self.finish(.failure(error))
+ }
+ }
+
+ let suiview = popoverContentController.view!
+ suiview.translatesAutoresizingMaskIntoConstraints = false
+
+ popoverContentController.modalPresentationStyle = .popover
+
+ if let popoverPresentationController = popoverContentController.popoverPresentationController {
+ popoverPresentationController.sourceView = presentingViewController.view
+ popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4)
+ popoverPresentationController.delegate = popoverContentController
+
+ DispatchQueue.main.async {
+ presentingViewController.present(popoverContentController, animated: true)
+ }
+ }else{
+ self.finish(.failure(
+ OperationError.invalidParameters("RemoveAppExtensionsOperation: popoverContentController.popoverPresentationController is nil"))
+ )
+ }
+ })
+
+ return alertController
+ }
+
+ struct ExtensionsInfo{
+ let excessInTarget: Set
+ let necessaryInExisting: Set
+ }
+
+ private func processExtensionsInfo(from targetAppBundle: ALTApplication,
+ localAppExtensions: Set?) -> Set
+ {
+ //App-Extensions: Ensure existing app's extensions in DB and currently installing app bundle's extensions must match
+ let targetAppEx: Set = targetAppBundle.appExtensions
+ let targetAppExNames = targetAppEx.map{ appEx in appEx.bundleIdentifier}
+
+ guard let extensionsInExistingApp = localAppExtensions else {
+ let diagnosticsMsg = "RemoveAppExtensionsOperation: ExistingApp is nil, Hence keeping all app extensions from targetAppBundle"
+ + "RemoveAppExtensionsOperation: ExistingAppEx: nil; targetAppBundleEx: \(targetAppExNames)"
+ print(diagnosticsMsg)
+ return Set() // nothing is excess since we are keeping all, so returning empty
+ }
+
+ let existingAppEx: Set = extensionsInExistingApp
+ let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier}
+
+ let excessExtensionsInTargetApp = targetAppEx.filter{
+ !(existingAppExNames.contains($0.bundleIdentifier))
+ }
+
+ let isMatching = (targetAppEx.count == existingAppEx.count) && excessExtensionsInTargetApp.isEmpty
+ let diagnosticsMsg = "RemoveAppExtensionsOperation: App Extensions in localAppBundle and targetAppBundle are matching: \(isMatching)\n"
+ + "RemoveAppExtensionsOperation: \nlocalAppBundleEx: \(existingAppExNames); \ntargetAppBundleEx: \(String(describing: targetAppExNames))\n"
+ print(diagnosticsMsg)
+
+ return excessExtensionsInTargetApp
+ }
+
+ private func backgroundModeExtensionsCleanup(excessExtensions: Set) {
+ // perform silent extensions cleanup for those that aren't already present in existing app
+ print("\n Performing background mode Extensions removal \n")
+ print("RemoveAppExtensionsOperation: Excess Extensions In TargetAppBundle: \(excessExtensions.map{$0.bundleIdentifier})")
+
+ do {
+ try Self.removeExtensions(from: excessExtensions)
+ return self.finish(.success(()))
+ } catch {
+ return self.finish(.failure(error))
+ }
+ }
+}
diff --git a/AltStore/Operations/ResignAppOperation.swift b/AltStore/Operations/ResignAppOperation.swift
index e4b899ad..7f165297 100644
--- a/AltStore/Operations/ResignAppOperation.swift
+++ b/AltStore/Operations/ResignAppOperation.swift
@@ -244,6 +244,7 @@ private extension ResignAppOperation
{
for case let fileURL as URL in enumerator
{
+ // for both sim and device, in debug mode builds, remove the tests bundles (if any)
#if DEBUG
guard !fileURL.lastPathComponent.lowercased().contains(".xctest") else {
// Remove embedded XCTest (+ dSYM) bundle from copied app bundle.
diff --git a/AltStore/Settings/Error Log/ConsoleLogView.swift b/AltStore/Settings/Error Log/ConsoleLogView.swift
new file mode 100644
index 00000000..772560cf
--- /dev/null
+++ b/AltStore/Settings/Error Log/ConsoleLogView.swift
@@ -0,0 +1,226 @@
+//
+// ConsoleLogView.swift
+// AltStore
+//
+// Created by Magesh K on 29/12/24.
+// Copyright © 2024 SideStore. All rights reserved.
+//
+import SwiftUI
+
+class ConsoleLogViewModel: ObservableObject {
+ @Published var logLines: [String] = []
+
+ @Published var searchTerm: String = ""
+ @Published var currentSearchIndex: Int = 0
+ @Published var searchResults: [Int] = [] // Stores indices of matching lines
+
+ private var fileWatcher: DispatchSourceFileSystemObject?
+
+ private let backgroundQueue = DispatchQueue(label: "com.myapp.backgroundQueue", qos: .background)
+ private var logURL: URL
+
+ init(logURL: URL) {
+ self.logURL = logURL
+ startFileWatcher() // Start monitoring the log file for changes
+ reloadLogData() // Load initial log data
+ }
+
+ private func startFileWatcher() {
+ let fileDescriptor = open(logURL.path, O_RDONLY)
+ guard fileDescriptor != -1 else {
+ print("Unable to open file for reading.")
+ return
+ }
+
+ fileWatcher = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: .write, queue: backgroundQueue)
+ fileWatcher?.setEventHandler {
+ self.reloadLogData()
+ }
+ fileWatcher?.resume()
+ }
+
+ private func reloadLogData() {
+ if let fileContents = try? String(contentsOf: logURL) {
+ let lines = fileContents.split(whereSeparator: \.isNewline).map { String($0) }
+ DispatchQueue.main.async {
+ self.logLines = lines
+ }
+ }
+ }
+
+ deinit {
+ fileWatcher?.cancel()
+ }
+
+
+ func performSearch() {
+ searchResults = logLines.enumerated()
+ .filter { $0.element.localizedCaseInsensitiveContains(searchTerm) }
+ .map { $0.offset }
+ }
+
+ func nextSearchResult() {
+ guard !searchResults.isEmpty else { return }
+ currentSearchIndex = (currentSearchIndex + 1) % searchResults.count
+ }
+
+ func previousSearchResult() {
+ guard !searchResults.isEmpty else { return }
+ currentSearchIndex = (currentSearchIndex - 1 + searchResults.count) % searchResults.count
+ }
+}
+
+
+public struct ConsoleLogView: View {
+
+ @ObservedObject var viewModel: ConsoleLogViewModel
+ @State private var scrollToBottom: Bool = false // State variable to trigger scroll
+ @State private var searchBarState: Bool = false
+ @FocusState private var isSearchFieldFocused: Bool
+
+ @State private var searchText: String = ""
+ @State private var scrollToIndex: Int?
+
+ private let resultHighlightColor = Color.orange
+ private let resultHighlightOpacity = 0.5
+ private let otherResultsColor = Color.yellow
+ private let otherResultsOpacity = 0.3
+
+ init(logURL: URL) {
+ self.viewModel = ConsoleLogViewModel(logURL: logURL)
+ }
+
+ public var body: some View {
+ VStack {
+
+ // Custom Header Bar (similar to QuickLook's preview screen)
+ HStack {
+ Text("Console Log")
+ .font(.system(size: 22, weight: .semibold))
+ .foregroundColor(.white)
+ Spacer()
+
+ if(!searchBarState){
+ SwiftUI.Button(action: {
+ searchBarState.toggle()
+ }) {
+ Image(systemName: "magnifyingglass")
+ .foregroundColor(.white)
+ .imageScale(.large)
+ }
+ .padding(.trailing)
+ }
+ SwiftUI.Button(action: {
+ scrollToBottom.toggle()
+ }) {
+ Image(systemName: "ellipsis")
+ .foregroundColor(.white)
+ .imageScale(.large)
+ }
+ }
+ .padding(15)
+ .padding(.top, 5)
+ .padding(.bottom, 2.5)
+ .background(Color.black.opacity(0.9))
+ .overlay(
+ Rectangle()
+ .frame(height: 1)
+ .foregroundColor(Color.gray.opacity(0.2)), alignment: .bottom
+ )
+
+ if(searchBarState){
+ // Search bar
+ HStack {
+ Image(systemName: "magnifyingglass")
+ .foregroundColor(.gray)
+ .padding(.trailing, 4)
+
+ TextField("Search", text: $searchText)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .onChange(of: searchText) { newValue in
+ viewModel.searchTerm = newValue
+ viewModel.performSearch()
+ }
+ .keyboardShortcut("f", modifiers: .command) // Focus search field
+
+ if !searchText.isEmpty {
+ // Search navigation buttons
+ SwiftUI.Button(action: {
+ viewModel.previousSearchResult()
+ scrollToIndex = viewModel.searchResults[viewModel.currentSearchIndex]
+ }) {
+ Image(systemName: "chevron.up")
+ }
+ .keyboardShortcut(.return, modifiers: [.command, .shift])
+ .disabled(viewModel.searchResults.isEmpty)
+
+ SwiftUI.Button(action: {
+ viewModel.nextSearchResult()
+ scrollToIndex = viewModel.searchResults[viewModel.currentSearchIndex]
+ }) {
+ Image(systemName: "chevron.down")
+ }
+ .keyboardShortcut(.return, modifiers: .command)
+ .disabled(viewModel.searchResults.isEmpty)
+
+ // Results counter
+ Text("\(viewModel.currentSearchIndex + 1)/\(viewModel.searchResults.count)")
+ .foregroundColor(.gray)
+ .font(.caption)
+ }
+
+ SwiftUI.Button(action: {
+ searchBarState.toggle()
+ }) {
+ Image(systemName: "xmark")
+ }
+ }
+ .padding(.horizontal, 15)
+ }
+
+
+
+ // Main Log Display (scrollable area)
+ ScrollView(.vertical) {
+ ScrollViewReader { proxy in
+ LazyVStack(alignment: .leading, spacing: 4) {
+ ForEach(viewModel.logLines.indices, id: \.self) { index in
+ Text(viewModel.logLines[index])
+ .font(.system(size: 12, design: .monospaced))
+ .foregroundColor(.white)
+ .background(
+ viewModel.searchResults.contains(index) ?
+ otherResultsColor.opacity(otherResultsOpacity) : Color.clear
+ )
+ .background(
+ viewModel.searchResults[safe: viewModel.currentSearchIndex] == index ?
+ resultHighlightColor.opacity(resultHighlightOpacity) : Color.clear
+ )
+ }
+ }
+ .onChange(of: scrollToIndex) { newIndex in
+ if let index = newIndex {
+ withAnimation {
+ proxy.scrollTo(index, anchor: .center)
+ }
+ }
+ }
+ .onChange(of: scrollToBottom) { _ in
+ viewModel.logLines.indices.last.map { last in
+ proxy.scrollTo(last, anchor: .bottom)
+ }
+ }
+ }
+ }
+ }
+ .background(Color.black) // Set background color to mimic QL's dark theme
+ .edgesIgnoringSafeArea(.all)
+ }
+}
+
+// Helper extension for safe array access
+extension Array {
+ subscript(safe index: Index) -> Element? {
+ indices.contains(index) ? self[index] : nil
+ }
+}
diff --git a/AltStore/Settings/Error Log/ErrorLogViewController.swift b/AltStore/Settings/Error Log/ErrorLogViewController.swift
index 498609c7..546299b2 100644
--- a/AltStore/Settings/Error Log/ErrorLogViewController.swift
+++ b/AltStore/Settings/Error Log/ErrorLogViewController.swift
@@ -16,6 +16,7 @@ import Roxas
import Nuke
import QuickLook
+import SwiftUI
final class ErrorLogViewController: UITableViewController, QLPreviewControllerDelegate
{
@@ -59,8 +60,46 @@ final class ErrorLogViewController: UITableViewController, QLPreviewControllerDe
// Assign just clearLogButton to hide export button.
self.navigationItem.rightBarButtonItems = [self.clearLogButton]
}
+
+// // Adjust the width of the right bar button items
+// adjustRightBarButtonWidth()
}
+
+// func adjustRightBarButtonWidth() {
+// // Access the current rightBarButtonItems
+// if let rightBarButtonItems = self.navigationItem.rightBarButtonItems {
+// for barButtonItem in rightBarButtonItems {
+// // Check if the button is a system button, and if so, replace it with a custom button
+// if barButtonItem.customView == nil {
+// // Replace with a custom UIButton for each bar button item
+// let customButton = UIButton(type: .custom)
+// if let image = barButtonItem.image {
+// customButton.setImage(image, for: .normal)
+// }
+// if let action = barButtonItem.action{
+// customButton.addTarget(barButtonItem.target, action: action, for: .touchUpInside)
+// }
+//
+// // Calculate the original size based on the system button
+// let originalSize = customButton.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
+//
+// let scaleFactor = 0.7
+//
+// // Scale the size by 0.7 (70%)
+// let scaledSize = CGSize(width: originalSize.width * scaleFactor, height: originalSize.height * scaleFactor)
+//
+// // Adjust the custom button's width
+//// customButton.frame.size = CGSize(width: 22, height: 22) // Adjust width as needed
+// customButton.frame.size = scaledSize // Adjust width as needed
+//
+// // Set the custom button as the custom view for the UIBarButtonItem
+// barButtonItem.customView = customButton
+// }
+// }
+// }
+// }
+
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard let loggedError = sender as? LoggedError, segue.identifier == "showErrorDetails" else { return }
@@ -216,15 +255,86 @@ private extension ErrorLogViewController
}
}
- @IBAction func showMinimuxerLogs(_ sender: UIBarButtonItem)
- {
- // Show minimuxer.log
- let previewController = QLPreviewController()
- previewController.dataSource = self
- let navigationController = UINavigationController(rootViewController: previewController)
- present(navigationController, animated: true, completion: nil)
+
+ enum LogView: String {
+ case consoleLog = "console-log"
+ case minimuxerLog = "minimuxer-log"
+
+ // This class will manage the QLPreviewController and the timer.
+ private class LogViewManager {
+ var previewController: QLPreviewController
+ var refreshTimer: Timer?
+ var logView: LogView
+
+ init(previewController: QLPreviewController, logView: LogView) {
+ self.previewController = previewController
+ self.logView = logView
+ }
+
+ // Start refreshing the preview controller every second
+ func startRefreshing() {
+ refreshTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(refreshPreview), userInfo: nil, repeats: true)
+ }
+
+ @objc private func refreshPreview() {
+ previewController.reloadData()
+ }
+
+ // Stop the timer to prevent leaks
+ func stopRefreshing() {
+ refreshTimer?.invalidate()
+ refreshTimer = nil
+ }
+
+ func updateLogPath() {
+ // Force the QLPreviewController to reload by changing the file path
+ previewController.reloadData()
+ }
+ }
+
+ // Method to get the QLPreviewController for this log type
+ func getViewController(_ dataSource: QLPreviewControllerDataSource) -> QLPreviewController {
+ let previewController = QLPreviewController()
+ previewController.restorationIdentifier = self.rawValue
+ previewController.dataSource = dataSource
+
+ // Create LogViewManager and start refreshing
+ let manager = LogViewManager(previewController: previewController, logView: self)
+// manager.startRefreshing() // DO NOT REFRESH the full view contents causing flickering
+
+ return previewController
+ }
+
+ func getLogPath() -> URL {
+ switch self {
+ case .consoleLog:
+ let appDelegate = UIApplication.shared.delegate as! AppDelegate
+ return appDelegate.consoleLog.logFileURL
+ case .minimuxerLog:
+ return FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log")
+ }
+ }
}
+ @IBAction func showConsoleLogs(_ sender: UIBarButtonItem) {
+ // Create the SwiftUI ConsoleLogView with the URL
+ let consoleLogView = ConsoleLogView(logURL: (UIApplication.shared.delegate as! AppDelegate).consoleLog.logFileURL)
+
+ // Create the UIHostingController
+ let consoleLogController = UIHostingController(rootView: consoleLogView)
+
+ // Configure the bottom sheet presentation
+ consoleLogController.modalPresentationStyle = .pageSheet
+ if let sheet = consoleLogController.sheetPresentationController {
+ sheet.detents = [.medium(), .large()] // You can adjust the size of the sheet (medium/large)
+ sheet.prefersGrabberVisible = true // Optional: Shows a grabber at the top of the sheet
+ sheet.selectedDetentIdentifier = .large // Default size when presented
+ }
+
+ // Present the bottom sheet
+ present(consoleLogController, animated: true, completion: nil)
+ }
+
@IBAction func clearLoggedErrors(_ sender: UIBarButtonItem)
{
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to clear the error log?", comment: ""), message: nil, preferredStyle: .actionSheet)
@@ -285,58 +395,74 @@ private extension ErrorLogViewController
self.performSegue(withIdentifier: "showErrorDetails", sender: loggedError)
}
- @available(iOS 15, *)
- @IBAction func exportDetailedLog(_ sender: UIBarButtonItem)
+ @IBAction func showMinimuxerLogs(_ sender: UIBarButtonItem)
{
- self.exportLogButton.isIndicatingActivity = true
-
- Task.detached(priority: .userInitiated) {
- do
- {
- let store = try OSLogStore(scope: .currentProcessIdentifier)
-
- // All logs since the app launched.
- let position = store.position(timeIntervalSinceLatestBoot: 0)
- let predicate = NSPredicate(format: "subsystem == %@", Logger.altstoreSubsystem)
-
- let entries = try store.getEntries(at: position, matching: predicate)
- .compactMap { $0 as? OSLogEntryLog }
- .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
-
- let outputText = entries.joined(separator: "\n")
-
- let outputDirectory = FileManager.default.uniqueTemporaryURL()
- try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
-
- let outputURL = outputDirectory.appendingPathComponent("altstore.log")
- try outputText.write(to: outputURL, atomically: true, encoding: .utf8)
-
- await MainActor.run {
- self._exportedLogURL = outputURL
-
- let previewController = QLPreviewController()
- previewController.delegate = self
- previewController.dataSource = self
- previewController.view.tintColor = .altPrimary
- self.present(previewController, animated: true)
- }
- }
- catch
- {
- Logger.main.error("Failed to export OSLog entries. \(error.localizedDescription, privacy: .public)")
-
- await MainActor.run {
- let alertController = UIAlertController(title: NSLocalizedString("Unable to Export Detailed Log", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
- alertController.addAction(.ok)
- self.present(alertController, animated: true)
- }
- }
-
- await MainActor.run {
- self.exportLogButton.isIndicatingActivity = false
- }
- }
+ // Show minimuxer.log
+ let previewController = LogView.minimuxerLog.getViewController(self)
+ let navigationController = UINavigationController(rootViewController: previewController)
+ present(navigationController, animated: true, completion: nil)
}
+
+// @available(iOS 15, *)
+// @IBAction func exportDetailedLog(_ sender: UIBarButtonItem)
+// {
+// self.exportLogButton.isIndicatingActivity = true
+//
+// Task.detached(priority: .userInitiated) {
+// do
+// {
+// let store = try OSLogStore(scope: .currentProcessIdentifier)
+//
+// // All logs since the app launched.
+// let position = store.position(timeIntervalSinceLatestBoot: 0)
+//// let predicate = NSPredicate(format: "subsystem == %@", Logger.altstoreSubsystem)
+////
+//// let entries = try store.getEntries(at: position, matching: predicate)
+//// .compactMap { $0 as? OSLogEntryLog }
+//// .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
+////
+// // Remove the predicate to get all log entries
+//// let entries = try store.getEntries(at: position)
+//// .compactMap { $0 as? OSLogEntryLog }
+//// .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
+//
+// let entries = try store.getEntries(at: position)
+//
+//// let outputText = entries.joined(separator: "\n")
+// let outputText = entries.map { $0.description }.joined(separator: "\n")
+//
+// let outputDirectory = FileManager.default.uniqueTemporaryURL()
+// try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
+//
+// let outputURL = outputDirectory.appendingPathComponent("altstore.log")
+// try outputText.write(to: outputURL, atomically: true, encoding: .utf8)
+//
+// await MainActor.run {
+// self._exportedLogURL = outputURL
+//
+// let previewController = QLPreviewController()
+// previewController.delegate = self
+// previewController.dataSource = self
+// previewController.view.tintColor = .altPrimary
+// self.present(previewController, animated: true)
+// }
+// }
+// catch
+// {
+// Logger.main.error("Failed to export OSLog entries. \(error.localizedDescription, privacy: .public)")
+//
+// await MainActor.run {
+// let alertController = UIAlertController(title: NSLocalizedString("Unable to Export Detailed Log", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
+// alertController.addAction(.ok)
+// self.present(alertController, animated: true)
+// }
+// }
+//
+// await MainActor.run {
+// self.exportLogButton.isIndicatingActivity = false
+// }
+// }
+// }
}
extension ErrorLogViewController
@@ -412,9 +538,13 @@ extension ErrorLogViewController: QLPreviewControllerDataSource {
return 1
}
- func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
- let fileURL = FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log")
- return fileURL as QLPreviewItem
+ func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem
+ {
+ guard let identifier = controller.restorationIdentifier,
+ let logView = LogView(rawValue: identifier) else {
+ fatalError("Invalid restorationIdentifier")
+ }
+ return logView.getLogPath() as QLPreviewItem
}
}
diff --git a/AltStore/Settings/OperationsLoggingContolView.swift b/AltStore/Settings/OperationsLoggingContolView.swift
new file mode 100644
index 00000000..84680809
--- /dev/null
+++ b/AltStore/Settings/OperationsLoggingContolView.swift
@@ -0,0 +1,284 @@
+//
+// SettingsView.swift
+// AltStore
+//
+// Created by Magesh K on 14/01/25.
+// Copyright © 2025 SideStore. All rights reserved.
+//
+
+
+import SwiftUI
+import AltStoreCore
+
+
+private final class DummyConformance: EnableJITContext
+{
+ private init(){} // non instantiatable
+ var installedApp: AltStoreCore.InstalledApp?
+ var error: (any Error)?
+}
+
+
+struct OperationsLoggingControlView: View {
+ let TITLE = "Operations Logging"
+ let BACKGROUND_COLOR = Color(.settingsBackground)
+
+ var viewModel = OperationsLoggingControl()
+
+ var body: some View {
+ NavigationView {
+ ZStack {
+// BACKGROUND_COLOR.ignoresSafeArea() // Force background to cover the entire screen
+ VStack{
+ Group{}.padding(12)
+
+ CustomList {
+ CustomSection(header: Text("Install Operations"))
+ {
+ CustomToggle("1. Authentication", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: AuthenticationOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: AuthenticationOperation.self, value: value)
+ }
+ ))
+
+ CustomToggle("2. VerifyAppPledge", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: VerifyAppPledgeOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: VerifyAppPledgeOperation.self, value: value)
+ }
+ ))
+
+ CustomToggle("3. DownloadApp", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: DownloadAppOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: DownloadAppOperation.self, value: value)
+ }
+ ))
+
+ CustomToggle("4. VerifyApp", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: VerifyAppOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: VerifyAppOperation.self, value: value)
+ }
+ ))
+
+ CustomToggle("5. RemoveAppExtensions", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: RemoveAppExtensionsOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: RemoveAppExtensionsOperation.self, value: value)
+ }
+ ))
+
+ CustomToggle("6. FetchAnisetteData", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: FetchAnisetteDataOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: FetchAnisetteDataOperation.self, value: value)
+ }
+ ))
+
+ CustomToggle("7. FetchProvisioningProfiles(I)", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: FetchProvisioningProfilesInstallOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: FetchProvisioningProfilesInstallOperation.self, value: value)
+ }
+ ))
+
+ CustomToggle("8. ResignApp", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: ResignAppOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: ResignAppOperation.self, value: value)
+ }
+ ))
+
+ CustomToggle("9. SendApp", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: SendAppOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: SendAppOperation.self, value: value)
+ }
+ ))
+
+ CustomToggle("10. InstallApp", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: InstallAppOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: InstallAppOperation.self, value: value)
+ }
+ ))
+ }
+
+ CustomSection(header: Text("Refresh Operations"))
+ {
+ CustomToggle("1. FetchProvisioningProfiles(R)", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: FetchProvisioningProfilesRefreshOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: FetchProvisioningProfilesRefreshOperation.self, value: value)
+ }
+ ))
+
+ CustomToggle("2. RefreshApp", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: RefreshAppOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: RefreshAppOperation.self, value: value)
+ }
+ ))
+ }
+
+ CustomSection(header: Text("AppIDs related Operations"))
+ {
+ CustomToggle("1. FetchAppIDs", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: FetchAppIDsOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: FetchAppIDsOperation.self, value: value)
+ }
+ ))
+ }
+
+ CustomSection(header: Text("Sources related Operations"))
+ {
+ CustomToggle("1. FetchSource", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: FetchSourceOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: FetchSourceOperation.self, value: value)
+ }
+ ))
+
+ CustomToggle("2. UpdateKnownSources", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: UpdateKnownSourcesOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: UpdateKnownSourcesOperation.self, value: value)
+ }
+ ))
+ }
+
+ CustomSection(header: Text("Backup Operations"))
+ {
+ CustomToggle("1. BackupApp", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: BackupAppOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: BackupAppOperation.self, value: value)
+ }
+ ))
+
+ CustomToggle("2. RemoveAppBackup", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: RemoveAppBackupOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: RemoveAppBackupOperation.self, value: value)
+ }
+ ))
+ }
+
+ CustomSection(header: Text("Activate/Deactive Operations"))
+ {
+ CustomToggle("1. RemoveApp", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: RemoveAppOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: RemoveAppOperation.self, value: value)
+ }
+ ))
+ CustomToggle("2. DeactivateApp", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: DeactivateAppOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: DeactivateAppOperation.self, value: value)
+ }
+ ))
+ }
+
+ CustomSection(header: Text("Background refresh Operations"))
+ {
+ CustomToggle("1. BackgroundRefreshApps", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: BackgroundRefreshAppsOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: BackgroundRefreshAppsOperation.self, value: value)
+ }
+ ))
+ }
+
+ CustomSection(header: Text("Enable JIT Operations"))
+ {
+ CustomToggle("1. EnableJIT", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: EnableJITOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: EnableJITOperation.self, value: value)
+ }
+ ))
+ }
+
+ CustomSection(header: Text("Patrons Operations"))
+ {
+ CustomToggle("1. UpdatePatrons", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: UpdatePatronsOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: UpdatePatronsOperation.self, value: value)
+ }
+ ))
+ }
+
+ CustomSection(header: Text("Cache Operations"))
+ {
+ CustomToggle("1. ClearAppCache", isOn: Binding(
+ get: { self.viewModel.getFromDatabase(for: ClearAppCacheOperation.self) },
+ set: { value in
+ self.viewModel.updateDatabase(for: ClearAppCacheOperation.self, value: value)
+ }
+ ))
+ }
+
+ CustomSection(header: Text("Misc Logging"))
+ {
+ CustomToggle("1. Anisette Internal Logging", isOn: Binding(
+ // enable anisette internal logging by default since it was already printing before
+ get: { OperationsLoggingControl.getUpdatedFromDatabase(
+ for: ANISETTE_VERBOSITY.self, defaultVal: true
+ )},
+ set: { value in
+ self.viewModel.updateDatabase(for: ANISETTE_VERBOSITY.self, value: value)
+ }
+ ))
+ }
+ }
+ }
+ }
+ .navigationTitle(TITLE)
+ }
+ .ignoresSafeArea(edges: .all)
+ }
+
+ private func CustomList(@ViewBuilder content: () -> Content) -> some View {
+// ScrollView {
+ List {
+ content()
+ }
+// .listStyle(.plain)
+// .listStyle(InsetGroupedListStyle()) // Or PlainListStyle for iOS 15
+// .background(Color.clear)
+// .background(Color(.settingsBackground))
+// .onAppear(perform: {
+// // cache the current background color
+// UITableView.appearance().backgroundColor = UIColor.red
+// })
+// .onDisappear(perform: {
+// // reset the background color to the cached value
+// UITableView.appearance().backgroundColor = UIColor.systemBackground
+// })
+ }
+
+ private func CustomSection(header: Text, @ViewBuilder content: () -> Content) -> some View {
+ Section(header: header) {
+ content()
+ }
+// .listRowBackground(Color.clear)
+ }
+
+ private func CustomToggle(_ title: String, isOn: Binding) -> some View {
+ Toggle(title, isOn: isOn)
+ .padding(3)
+// .foregroundColor(.white) // Ensures text color is always white
+// .font(.headline)
+ }
+}
+
+struct SettingsView_Previews: PreviewProvider {
+ static var previews: some View {
+ OperationsLoggingControlView()
+ }
+}
diff --git a/AltStore/Settings/PatreonViewController.swift b/AltStore/Settings/PatreonViewController.swift
index 92f7bdb5..dd945161 100644
--- a/AltStore/Settings/PatreonViewController.swift
+++ b/AltStore/Settings/PatreonViewController.swift
@@ -318,7 +318,8 @@ extension PatreonViewController
case .none: footerView.button.isIndicatingActivity = true
case .success?: footerView.button.isHidden = true
case .failure?:
- #if DEBUG
+ // In simulator debug builds only enable debug mode flag
+ #if DEBUG && targetEnvironment(simulator)
let debug = true
#else
let debug = false
diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard
index beede7ed..7a7baddf 100644
--- a/AltStore/Settings/Settings.storyboard
+++ b/AltStore/Settings/Settings.storyboard
@@ -4,6 +4,7 @@
+
@@ -21,7 +22,7 @@
-
+
-
-
+
+
-
+
+
-
+
-
+
+
-
+
@@ -1539,6 +1679,8 @@ Settings by i cons from the Noun Project
+
+
diff --git a/AltStore/Settings/SettingsViewController.swift b/AltStore/Settings/SettingsViewController.swift
index 832a59ec..88e09eb4 100644
--- a/AltStore/Settings/SettingsViewController.swift
+++ b/AltStore/Settings/SettingsViewController.swift
@@ -27,7 +27,9 @@ extension SettingsViewController
case instructions
case techyThings
case credits
- case debug
+ case advancedSettings
+ // diagnostics section, will be enabled on release builds only on swipe down with 3 fingers 3 times
+ case diagnostics
// case macDirtyCow
}
@@ -35,17 +37,17 @@ extension SettingsViewController
{
case backgroundRefresh
case noIdleTimeout
- @available(iOS 14, *)
case addToSiri
case disableAppLimit
static var allCases: [AppRefreshRow] {
- var c: [AppRefreshRow] = [.backgroundRefresh, .noIdleTimeout]
- guard #available(iOS 14, *) else { return c }
- c.append(.addToSiri)
+ var c: [AppRefreshRow] = [.backgroundRefresh, .noIdleTimeout, .addToSiri]
// conditional entries go at the last to preserve ordering
- if !ProcessInfo().sparseRestorePatched { c.append(.disableAppLimit) }
+ if UserDefaults.standard.isCowExploitSupported || !ProcessInfo().sparseRestorePatched
+ {
+ c.append(.disableAppLimit)
+ }
return c
}
}
@@ -64,17 +66,25 @@ extension SettingsViewController
case clearCache
}
- fileprivate enum DebugRow: Int, CaseIterable
+ fileprivate enum AdvancedSettingsRow: Int, CaseIterable
{
case sendFeedback
case refreshAttempts
case refreshSideJITServer
case resetPairingFile
case anisetteServers
- case responseCaching
case betaUpdates
- case resignedAppExport
-// case advancedSettings
+// case hiddenSettings
+ }
+
+ fileprivate enum DiagnosticsRow: Int, CaseIterable
+ {
+ case responseCaching
+ case exportResignedApp
+ case verboseOperationsLogging
+ case exportSqliteDB
+ case operationsLoggingControl
+ case minimuxerConsoleLogging
}
}
@@ -94,10 +104,12 @@ final class SettingsViewController: UITableViewController
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
@IBOutlet private var noIdleTimeoutSwitch: UISwitch!
@IBOutlet private var disableAppLimitSwitch: UISwitch!
- @IBOutlet private var isBetaUpdatesEnabled: UISwitch!
- @IBOutlet private var isResignedAppExportEnabled: UISwitch!
+ @IBOutlet private var betaUpdatesSwitch: UISwitch!
+ @IBOutlet private var exportResignedAppsSwitch: UISwitch!
+ @IBOutlet private var verboseOperationsLoggingSwitch: UISwitch!
+ @IBOutlet private var minimuxerConsoleLoggingSwitch: UISwitch!
- @IBOutlet private var refreshSideJITServer: UILabel!
+// @IBOutlet private var refreshSideJITServer: UILabel!
@IBOutlet private var disableResponseCachingSwitch: UISwitch!
@IBOutlet private var mastodonButton: UIButton!
@@ -111,6 +123,8 @@ final class SettingsViewController: UITableViewController
return .lightContent
}
+ private var exportDBInProgress = false
+
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
@@ -127,53 +141,15 @@ final class SettingsViewController: UITableViewController
self.prototypeHeaderFooterView = nib.instantiate(withOwner: nil, options: nil)[0] as? SettingsHeaderFooterView
self.tableView.register(nib, forHeaderFooterViewReuseIdentifier: "HeaderFooterView")
-
- let debugModeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(SettingsViewController.handleDebugModeGesture(_:)))
- debugModeGestureRecognizer.delegate = self
- debugModeGestureRecognizer.direction = .up
- debugModeGestureRecognizer.numberOfTouchesRequired = 3
- self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
-
- var versionString: String = ""
- if let installedApp = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext)
- {
- #if BETA
- // Only show build version for BETA builds.
- let localizedVersion = if let bundleVersion = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as? String {
- "\(installedApp.version) (\(bundleVersion))"
- } else {
- installedApp.localizedVersion
- }
- #else
- let localizedVersion = installedApp.version
- #endif
-
- self.versionLabel.text = NSLocalizedString(String(format: "Version %@", localizedVersion), comment: "SideStore Version")
- }
- else if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
- {
- versionString += "SideStore \(version)"
- if let xcode = Bundle.main.object(forInfoDictionaryKey: "DTXcode") as? String {
- versionString += " - Xcode \(xcode) - "
- if let build = Bundle.main.object(forInfoDictionaryKey: "DTXcodeBuild") as? String {
- versionString += "\(build)"
- }
- }
- if let pairing = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String {
- let pair_test = pairing == ""
- if !pair_test {
- versionString += " - \(!pair_test)"
- }
- }
- self.versionLabel.text = NSLocalizedString(String(format: "Version %@", version), comment: "SideStore Version")
- }
- else
- {
- self.versionLabel.text = nil
- versionString += "SideStore\t"
- versionString += "\n\(Bundle.Info.appbundleIdentifier)"
- self.versionLabel.text = NSLocalizedString(versionString, comment: "SideStore Version")
- }
+
+ let debugModeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(SettingsViewController.handleDebugModeGesture(_:)))
+ debugModeGestureRecognizer.delegate = self
+ debugModeGestureRecognizer.direction = .up
+ debugModeGestureRecognizer.numberOfTouchesRequired = 3
+ self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
+
+ // set the version label to show in settings screen
+ self.versionLabel.text = getVersionLabel()
self.versionLabel.numberOfLines = 0
self.versionLabel.lineBreakMode = .byWordWrapping
@@ -233,6 +209,74 @@ final class SettingsViewController: UITableViewController
private extension SettingsViewController
{
+
+ private func getVersionLabel() -> String {
+ let MARKETING_VERSION_KEY = "CFBundleShortVersionString"
+ let BUILD_REVISION = "CFBundleRevision" // commit ID for now (but could be any, set by build env vars
+ let CURRENT_PROJECT_VERSION = kCFBundleVersionKey as String
+
+ func getXcodeVersion() -> String {
+ let XCODE_VERSION = "DTXcode"
+ let XCODE_REVISION = "DTXcodeBuild"
+
+ let xcode = Bundle.main.object(forInfoDictionaryKey: XCODE_VERSION) as? String
+ let build = Bundle.main.object(forInfoDictionaryKey: XCODE_REVISION) as? String
+
+ var xcodeVersion = xcode.map { version in
+// " - Xcode \(version) - " + (build.map { revision in "\(revision)" } ?? "") // Ex: "0.6.0 - Xcode 16.2 - 21ac1ef"
+ "Xcode \(version) - " + (build.map { revision in "\(revision)" } ?? "") // Ex: "0.6.0 - Xcode 16.2 - 21ac1ef"
+ } ?? ""
+
+ if let pairing = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String,
+ pairing != ""{
+ xcodeVersion += " - true"
+ }
+ return xcodeVersion
+ }
+
+ var versionLabel: String = ""
+
+ if let installedApp = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext)
+ {
+ #if BETA
+ // Only show build version (and build revision) for BETA builds.
+ let bundleVersion: String? = Bundle.main.object(forInfoDictionaryKey: CURRENT_PROJECT_VERSION) as? String
+ let buildRevision: String? = Bundle.main.object(forInfoDictionaryKey: BUILD_REVISION) as? String
+
+ var localizedVersion = bundleVersion.map { version in
+ "\(installedApp.version) (\(version))" + (buildRevision.map { revision in " - \(revision)" } ?? "") // Ex: "0.6.0 (0600) - 1acdef3"
+ } ?? installedApp.localizedVersion
+
+ #else
+ var localizedVersion = installedApp.version
+ #endif
+
+ versionLabel = NSLocalizedString(String(format: "Version %@", localizedVersion), comment: "SideStore Version")
+ }
+ else if let version = Bundle.main.object(forInfoDictionaryKey: MARKETING_VERSION_KEY) as? String
+ {
+ var version = "SideStore \(version)"
+
+ version += getXcodeVersion()
+
+ versionLabel = NSLocalizedString(String(format: "Version %@", version), comment: "SideStore Version")
+ }
+ else
+ {
+ var version = "SideStore\t"
+ version += "\n\(Bundle.Info.appbundleIdentifier)"
+ versionLabel = NSLocalizedString(version, comment: "SideStore Version")
+ }
+
+ // add xcode build version if in debug mode
+ #if DEBUG
+ versionLabel += "\n\(getXcodeVersion())"
+ #endif
+
+ return versionLabel
+ }
+
+
func update()
{
if let team = DatabaseManager.shared.activeTeam()
@@ -248,12 +292,19 @@ private extension SettingsViewController
self.activeTeam = nil
}
+ // AppRefreshRow
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
self.noIdleTimeoutSwitch.isOn = UserDefaults.standard.isIdleTimeoutDisableEnabled
self.disableAppLimitSwitch.isOn = UserDefaults.standard.isAppLimitDisabled
+
+ // AdvancedSettingsRow
+ self.betaUpdatesSwitch.isOn = UserDefaults.standard.isBetaUpdatesEnabled
+
+ // DiagnosticsRow
self.disableResponseCachingSwitch.isOn = UserDefaults.standard.responseCachingDisabled
- self.isBetaUpdatesEnabled.isOn = UserDefaults.standard.isBetaUpdatesEnabled
- self.isResignedAppExportEnabled.isOn = UserDefaults.standard.isResignedAppExportEnabled
+ self.exportResignedAppsSwitch.isOn = UserDefaults.standard.isExportResignedAppEnabled
+ self.verboseOperationsLoggingSwitch.isOn = UserDefaults.standard.isVerboseOperationsLoggingEnabled
+ self.minimuxerConsoleLoggingSwitch.isOn = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
if self.isViewLoaded
{
@@ -335,6 +386,12 @@ private extension SettingsViewController
case .credits:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("CREDITS", comment: "")
+ case .advancedSettings:
+ settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("ADVANCED SETTINGS", comment: "")
+
+ case .diagnostics:
+ settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("DIAGNOSTICS", comment: "")
+
// case .macDirtyCow:
// if isHeader
// {
@@ -345,8 +402,6 @@ private extension SettingsViewController
// settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("If you've removed the 3-sideloaded app limit via the MacDirtyCow exploit, disable this setting to sideload more than 3 apps at a time.", comment: "")
// }
- case .debug:
- settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("DEBUG", comment: "")
}
}
@@ -425,16 +480,32 @@ private extension SettingsViewController
}
@IBAction func toggleDisableAppLimit(_ sender: UISwitch) {
- UserDefaults.standard.isAppLimitDisabled = sender.isOn
- if UserDefaults.standard.activeAppsLimit != nil
- {
- UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
+ if UserDefaults.standard.isCowExploitSupported || !ProcessInfo().sparseRestorePatched {
+ // accept state change only when valid
+ UserDefaults.standard.isAppLimitDisabled = sender.isOn
+
+ // TODO: Here we force reload the activeAppsLimit after detecting change in isAppLimitDisabled
+ // Why do we need to do this, once identified if this is intentional and working as expected, remove this todo
+ if UserDefaults.standard.activeAppsLimit != nil
+ {
+ UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
+ }
}
}
@IBAction func toggleResignedAppExport(_ sender: UISwitch) {
// update it in database
- UserDefaults.standard.isResignedAppExportEnabled = sender.isOn
+ UserDefaults.standard.isExportResignedAppEnabled = sender.isOn
+ }
+
+ @IBAction func toggleVerboseOperationsLogging(_ sender: UISwitch) {
+ // update it in database
+ UserDefaults.standard.isVerboseOperationsLoggingEnabled = sender.isOn
+ }
+
+ @IBAction func toggleMinimuxerConsoleLogging(_ sender: UISwitch) {
+ // update it in database
+ UserDefaults.standard.isMinimuxerConsoleLoggingEnabled = sender.isOn
}
@IBAction func toggleEnableBetaUpdates(_ sender: UISwitch) {
@@ -457,7 +528,7 @@ private extension SettingsViewController
UserDefaults.standard.responseCachingDisabled = sender.isOn
}
- @IBAction func addRefreshAppsShortcut()
+ func addRefreshAppsShortcut()
{
guard let shortcut = INShortcut(intent: INInteraction.refreshAllApps().intent) else { return }
@@ -679,8 +750,7 @@ extension SettingsViewController
case _ where isSectionHidden(section): return nil
case .signIn where self.activeTeam != nil: return nil
case .account where self.activeTeam == nil: return nil
- // case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug:
- case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .debug:
+ case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .advancedSettings, .diagnostics /* ,.macDirtyCow */:
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(headerView, for: section, isHeader: true)
return headerView
@@ -702,7 +772,7 @@ extension SettingsViewController
self.prepare(footerView, for: section, isHeader: false)
return footerView
- case .account, .credits, .debug, .instructions: return nil
+ case .account, .credits, .advancedSettings, .instructions, .diagnostics: return nil
}
}
@@ -714,8 +784,8 @@ extension SettingsViewController
case _ where isSectionHidden(section): return 1.0
case .signIn where self.activeTeam != nil: return 1.0
case .account where self.activeTeam == nil: return 1.0
- // case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug:
- case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .debug:
+ // case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .macDirtyCow, .advanced:
+ case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .advancedSettings, .diagnostics:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true)
return height
@@ -732,11 +802,11 @@ extension SettingsViewController
case .signIn where self.activeTeam != nil: return 1.0
case .account where self.activeTeam == nil: return 1.0
// case .signIn, .patreon, .display, .appRefresh, .techyThings, .macDirtyCow:
- case .signIn, .patreon, .display, .appRefresh, .techyThings:
+ case .signIn, .patreon, .display, .appRefresh, .techyThings, .diagnostics:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
return height
- case .account, .credits, .debug, .instructions: return 0.0
+ case .account, .credits, .advancedSettings, .instructions: return 0.0
}
}
}
@@ -757,7 +827,7 @@ extension SettingsViewController
case .noIdleTimeout: break
case .disableAppLimit: break
case .addToSiri:
- guard #available(iOS 14, *) else { return }
+// guard #available(iOS 14, *) else { return } // our min deployment is iOS 15 now :) so commented out
self.addRefreshAppsShortcut()
}
@@ -784,8 +854,8 @@ extension SettingsViewController
self.tableView.deselectRow(at: selectedIndexPath, animated: true)
}
- case .debug:
- let row = DebugRow.allCases[indexPath.row]
+ case .advancedSettings:
+ let row = AdvancedSettingsRow.allCases[indexPath.row]
switch row
{
case .sendFeedback:
@@ -823,11 +893,11 @@ extension SettingsViewController
}
self.present(mailViewController, animated: true, completion: nil)
- } else {
+ } else {
let toastView = ToastView(text: NSLocalizedString("Cannot Send Mail", comment: ""), detailText: nil)
toastView.show(in: self)
- }
- })
+ }
+ })
// Cancel action
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
@@ -995,7 +1065,7 @@ extension SettingsViewController
self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: anisetteServersController), sender: nil)
-// case .advancedSettings:
+// case .hiddenSettings:
// // Create the URL that deep links to your app's custom settings.
// if let url = URL(string: UIApplication.openSettingsURLString) {
// // Ask the system to open that URL.
@@ -1003,9 +1073,49 @@ extension SettingsViewController
// } else {
// ELOG("UIApplication.openSettingsURLString invalid")
// }
- case .refreshAttempts, .responseCaching, .betaUpdates, .resignedAppExport : break
+ case .refreshAttempts, .betaUpdates : break
}
+
+ case .diagnostics:
+ let row = DiagnosticsRow.allCases[indexPath.row]
+ switch row {
+
+ case .exportSqliteDB:
+ // do not accept simulatenous export requests
+ if !exportDBInProgress {
+ exportDBInProgress = true
+ Task{
+ var toastView: ToastView?
+ do{
+ let exportedURL = try await CoreDataHelper.exportCoreDataStore()
+ print("exportSqliteDB: ExportedURL: \(exportedURL)")
+ toastView = ToastView(text: "Export Successful", detailText: nil)
+ }catch{
+ print("exportSqliteDB: \(error)")
+ toastView = ToastView(error: error)
+ }
+
+ // show toast to user about the result
+ DispatchQueue.main.async {
+ toastView?.show(in: self)
+ }
+
+ // update that work has finished
+ exportDBInProgress = false
+ }
+ }
+
+ case .operationsLoggingControl:
+ // Instantiate SwiftUI View inside UIHostingController
+ let operationsLoggingControlView = OperationsLoggingControlView()
+ let operationsLoggingController = UIHostingController(rootView: operationsLoggingControlView)
+ let segue = UIStoryboardSegue(identifier: "operationsLoggingControl", source: self, destination: operationsLoggingController)
+ self.present(segue.destination, animated: true, completion: nil)
+
+ case .responseCaching, .exportResignedApp, .verboseOperationsLogging, .minimuxerConsoleLogging : break
+ }
+
// case .account, .patreon, .display, .instructions, .macDirtyCow: break
case .account, .patreon, .display, .instructions: break
diff --git a/AltStore/Extensions/ProcessInfo+SideStore.swift b/AltStoreCore/Extensions/ProcessInfo+AltStore.swift
similarity index 96%
rename from AltStore/Extensions/ProcessInfo+SideStore.swift
rename to AltStoreCore/Extensions/ProcessInfo+AltStore.swift
index 9b8c9084..5c986795 100644
--- a/AltStore/Extensions/ProcessInfo+SideStore.swift
+++ b/AltStoreCore/Extensions/ProcessInfo+AltStore.swift
@@ -71,20 +71,20 @@ fileprivate struct BuildVersion: Comparable {
}
extension ProcessInfo {
- var shortVersion: String {
+ public var shortVersion: String {
operatingSystemVersionString
.replacingOccurrences(of: "Version ", with: "")
.replacingOccurrences(of: "Build ", with: "")
}
- var operatingSystemBuild: String {
+ public var operatingSystemBuild: String {
if let start = shortVersion.range(of: "(")?.upperBound,
let end = shortVersion.range(of: ")")?.lowerBound {
shortVersion[start.. OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 1) { true }
else if operatingSystemVersion >= OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 0),
diff --git a/AltStoreCore/Extensions/UserDefaults+AltStore.swift b/AltStoreCore/Extensions/UserDefaults+AltStore.swift
index abeee047..b885166e 100644
--- a/AltStoreCore/Extensions/UserDefaults+AltStore.swift
+++ b/AltStoreCore/Extensions/UserDefaults+AltStore.swift
@@ -33,7 +33,9 @@ public extension UserDefaults
@NSManaged var isIdleTimeoutDisableEnabled: Bool
@NSManaged var isAppLimitDisabled: Bool
@NSManaged var isBetaUpdatesEnabled: Bool
- @NSManaged var isResignedAppExportEnabled: Bool
+ @NSManaged var isExportResignedAppEnabled: Bool
+ @NSManaged var isVerboseOperationsLoggingEnabled: Bool
+ @NSManaged var isMinimuxerConsoleLoggingEnabled: Bool
@NSManaged var isPairingReset: Bool
@NSManaged var isDebugModeEnabled: Bool
@NSManaged var presentedLaunchReminderNotification: Bool
@@ -106,11 +108,12 @@ public extension UserDefaults
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios15_7_2)) ||
(ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16) && !ProcessInfo.processInfo.isOperatingSystemAtLeast(ios16_2))
- #if DEBUG
- let permissionCheckingDisabled = true
- #else
+ // TODO: @mahee96: why should the permissions checking be any different, for now, it shouldn't so commented debug mode code
+// #if DEBUG
+// let permissionCheckingDisabled = true
+// #else
let permissionCheckingDisabled = false
- #endif
+// #endif
// Pre-iOS 15 doesn't support custom sorting, so default to sorting by name.
// Otherwise, default to `default` sorting (a.k.a. "source order").
@@ -119,7 +122,10 @@ public extension UserDefaults
let defaults = [
#keyPath(UserDefaults.isAppLimitDisabled): false,
#keyPath(UserDefaults.isBetaUpdatesEnabled): false,
- #keyPath(UserDefaults.isResignedAppExportEnabled): false,
+ #keyPath(UserDefaults.isExportResignedAppEnabled): false,
+ #keyPath(UserDefaults.isDebugModeEnabled): false,
+ #keyPath(UserDefaults.isVerboseOperationsLoggingEnabled): false,
+ #keyPath(UserDefaults.isMinimuxerConsoleLoggingEnabled): true, // minimuxer logging is enabled by default as before
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
#keyPath(UserDefaults.isIdleTimeoutDisableEnabled): true,
#keyPath(UserDefaults.isPairingReset): true,
@@ -137,7 +143,8 @@ public extension UserDefaults
UserDefaults.standard.register(defaults: defaults)
UserDefaults.shared.register(defaults: defaults)
- if !isMacDirtyCowSupported
+ // MDC is unsupported and spareRestore is patched
+ if !isMacDirtyCowSupported && ProcessInfo().sparseRestorePatched
{
// Disable isAppLimitDisabled if running iOS version that doesn't support MacDirtyCow.
UserDefaults.standard.isAppLimitDisabled = false
diff --git a/AltStoreCore/Model/DatabaseManager.swift b/AltStoreCore/Model/DatabaseManager.swift
index a3758453..cdc89656 100644
--- a/AltStoreCore/Model/DatabaseManager.swift
+++ b/AltStoreCore/Model/DatabaseManager.swift
@@ -87,7 +87,8 @@ public extension DatabaseManager
guard !self.isStarted else { return finish(nil) }
- #if DEBUG
+ // In simulator, when previews are generated, it initializes the db, in doing so this removal may be required
+ #if DEBUG && targetEnvironment(simulator)
// Wrap in #if DEBUG to *ensure* we never accidentally delete production databases.
if ProcessInfo.processInfo.isPreview
{
@@ -354,7 +355,8 @@ private extension DatabaseManager
let fileURL = installedApp.fileURL
- #if DEBUG
+ // @mahee96: it shouldn't matter if it is debug/release, the file is expected to be in its place (except for simulator probably coz it doesn't suppor app installs anyway)
+ #if DEBUG && targetEnvironment(simulator)
let replaceCachedApp = true
#else
let replaceCachedApp = !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version || installedApp.buildVersion != localApp.buildVersion
diff --git a/AltWidget/Assets.xcassets/SideStore.imageset/Contents.json b/AltWidget/Assets.xcassets/SideStore.imageset/Contents.json
index 161b1833..db1017b1 100644
--- a/AltWidget/Assets.xcassets/SideStore.imageset/Contents.json
+++ b/AltWidget/Assets.xcassets/SideStore.imageset/Contents.json
@@ -1,6 +1,7 @@
{
"images" : [
{
+ "filename" : "1024.png",
"idiom" : "universal",
"scale" : "1x"
},
diff --git a/AltWidget/Extensions/View+AltWidget.swift b/AltWidget/Extensions/View+AltWidget.swift
index 74dc4231..ce04c510 100644
--- a/AltWidget/Extensions/View+AltWidget.swift
+++ b/AltWidget/Extensions/View+AltWidget.swift
@@ -53,4 +53,29 @@ extension View
self
}
}
+
+ @ViewBuilder
+ func pageUpButton(_ widgetID: Int?, _ widgetKind: String) -> some View {
+ if #available(iOSApplicationExtension 17, *) {
+ Button(intent: PaginationIntent(widgetID, .up, widgetKind)){
+ self
+ }
+ .buttonStyle(.plain)
+ } else {
+ self
+ }
+ }
+
+ @ViewBuilder
+ func pageDownButton(_ widgetID: Int?, _ widgetKind: String) -> some View {
+ if #available(iOSApplicationExtension 17, *) {
+ Button(intent: PaginationIntent(widgetID, .down, widgetKind)){
+ self
+ }
+ .buttonStyle(.plain)
+ } else {
+ self
+ }
+ }
+
}
diff --git a/AltWidget/Intents/PaginationIntent.swift b/AltWidget/Intents/PaginationIntent.swift
new file mode 100644
index 00000000..f68df42c
--- /dev/null
+++ b/AltWidget/Intents/PaginationIntent.swift
@@ -0,0 +1,71 @@
+//
+// PaginationIntent.swift
+// AltStore
+//
+// Created by Magesh K on 08/01/25.
+// Copyright © 2025 SideStore. All rights reserved.
+//
+
+import AppIntents
+import Intents
+import WidgetKit
+
+public enum Direction: String, Sendable{
+ case up
+ case down
+}
+
+public struct NavigationEvent {
+ let direction: Direction?
+ var consumed: Bool = false
+ var dataHolder: PaginationDataHolder?
+}
+
+@available(iOS 17, *)
+class PaginationIntent: AppIntent, @unchecked Sendable {
+
+ static var title: LocalizedStringResource = "Page Navigation Intent"
+ static var isDiscoverable: Bool = false
+
+ @Parameter(title: "widgetID")
+ var widgetID: Int
+
+ @Parameter(title: "Direction")
+ var direction: String
+
+ @Parameter(title: "widgetKind")
+ var widgetKind: String
+
+ required init(){}
+
+ // NOTE: widgetID here means the configurable value using edit widget button
+ // but widgetKind is the kind set in when instantiating the widget configuration
+ init(_ widgetID: Int?, _ direction: Direction, _ widgetKind: String){
+ // if id was not passed in, then we assume the widget isn't customized yet
+ // hence we use the common ID, if this is not present in registry of PageInfoManager
+ // then it will return nil, triggering to show first page in the provider
+ self.widgetID = widgetID ?? WidgetUpdateIntent.COMMON_WIDGET_ID
+ self.direction = direction.rawValue
+ self.widgetKind = widgetKind
+ }
+
+ func perform() async throws -> some IntentResult {
+ // Post the navigation event into the shared db (Dictionary) and ask to reload
+ DispatchQueue(label: String(widgetID)).sync {
+ self.postThisNavigationEvent()
+ WidgetCenter.shared.reloadTimelines(ofKind: widgetKind)
+ }
+ return .result()
+ }
+
+ private func postThisNavigationEvent(){
+ // re-use an existing event if available and update only required parts
+ let navEvent = PageInfoManager.shared.getPageInfo(forWidgetKind: widgetKind, forWidgetID: widgetID)
+ let navigationEvent = NavigationEvent(
+ direction: Direction(rawValue: direction),
+ consumed: false, // event is never consumed at origin :D
+ dataHolder: navEvent?.dataHolder ?? nil
+ )
+ PageInfoManager.shared.setPageInfo(forWidgetKind: widgetKind, forWidgetID: widgetID, value: navigationEvent)
+ }
+}
diff --git a/AltWidget/Intents/WidgetUpdateIntent.swift b/AltWidget/Intents/WidgetUpdateIntent.swift
new file mode 100644
index 00000000..503c5e3e
--- /dev/null
+++ b/AltWidget/Intents/WidgetUpdateIntent.swift
@@ -0,0 +1,20 @@
+//
+// WidgetUpdateIntent.swift
+// AltStore
+//
+// Created by Magesh K on 10/01/25.
+// Copyright © 2025 SideStore. All rights reserved.
+//
+
+import AppIntents
+
+@available(iOS 17, *)
+final class WidgetUpdateIntent: WidgetConfigurationIntent, @unchecked Sendable {
+ public static let COMMON_WIDGET_ID = 1
+
+ static var title: LocalizedStringResource { "Widget ID update Intent" }
+ static var isDiscoverable: Bool { false }
+
+ @Parameter(title: "ID", description: "Provide a numeric ID to identify the widget", default: 1)
+ var ID: Int?
+}
diff --git a/AltWidget/Manager/PageInfoManager.swift b/AltWidget/Manager/PageInfoManager.swift
new file mode 100644
index 00000000..791a3585
--- /dev/null
+++ b/AltWidget/Manager/PageInfoManager.swift
@@ -0,0 +1,44 @@
+//
+// PageInfoManager.swift
+// AltStore
+//
+// Created by Magesh K on 11/01/25.
+// Copyright © 2025 SideStore. All rights reserved.
+//
+
+import Foundation
+
+// TODO: See if we can persist these values instead of keeping in memory to prevent memory leaks
+// Possible ways: Userdefaults.standard - set/get ?
+class PageInfoManager {
+ static var shared = PageInfoManager()
+ private var pageInfoMap: [String: NavigationEvent] = [:]
+
+ private init() {}
+
+ private func getKey(forWidgetKind kind: String, forWidgetID id: Int) -> String{
+ return "\(kind)@\(id)"
+ }
+
+ func setPageInfo(forWidgetKind kind: String, forWidgetID id: Int, value: NavigationEvent?) {
+ let key = getKey(forWidgetKind: kind, forWidgetID: id)
+// UserDefaults.standard.set(value, forKey: key)
+ pageInfoMap[key] = value
+ }
+
+ func getPageInfo(forWidgetKind kind: String, forWidgetID id: Int) -> NavigationEvent? {
+ let key = getKey(forWidgetKind: kind, forWidgetID: id)
+// return UserDefaults.standard.value(forKey: key)
+ return pageInfoMap[key]
+
+ }
+
+ func popPageInfo(forWidgetKind kind: String, forWidgetID id: Int) -> NavigationEvent? {
+ let key = getKey(forWidgetKind: kind, forWidgetID: id)
+ return pageInfoMap.removeValue(forKey: key)
+ }
+
+ func clearAll() {
+ pageInfoMap.removeAll()
+ }
+}
diff --git a/AltWidget/Providers/ActiveAppsTimelineProvider+Simulator.swift b/AltWidget/Providers/ActiveAppsTimelineProvider+Simulator.swift
new file mode 100644
index 00000000..e319d6db
--- /dev/null
+++ b/AltWidget/Providers/ActiveAppsTimelineProvider+Simulator.swift
@@ -0,0 +1,36 @@
+//
+// ActiveAppsTimelineProvider+Simulator.swift
+// AltStore
+//
+// Created by Magesh K on 10/01/25.
+// Copyright © 2025 SideStore. All rights reserved.
+//
+
+
+/// Simulator data generator
+#if targetEnvironment(simulator)
+@available(iOS 17, *)
+extension ActiveAppsTimelineProvider {
+
+ func getSimulatedData(apps: [AppSnapshot]) -> [AppSnapshot]{
+ var apps = apps
+ var newSets: [AppSnapshot] = []
+ // this dummy data is for simulator (uncomment when testing ActiveAppsWidget pagination)
+ if (apps.count > 0){
+ let app = apps[0]
+ for i in 1...10 {
+ let name = "\(app.name) - \(i)"
+ let x = AppSnapshot(name: name,
+ bundleIdentifier: app.bundleIdentifier,
+ expirationDate: app.expirationDate,
+ refreshedDate: app.refreshedDate
+ )
+ newSets.append(x)
+ }
+ apps = newSets
+ }
+ return apps
+ }
+}
+#endif
+
diff --git a/AltWidget/Providers/ActiveAppsTimelineProvider.swift b/AltWidget/Providers/ActiveAppsTimelineProvider.swift
new file mode 100644
index 00000000..465222ae
--- /dev/null
+++ b/AltWidget/Providers/ActiveAppsTimelineProvider.swift
@@ -0,0 +1,115 @@
+//
+// ActiveAppsTimelineProvider.swift
+// AltStore
+//
+// Created by Magesh K on 10/01/25.
+// Copyright © 2025 SideStore. All rights reserved.
+//
+
+import WidgetKit
+
+protocol WidgetInfo{
+ var ID: Int? { get }
+}
+
+@available(iOS 17, *)
+class ActiveAppsTimelineProvider: AppsTimelineProviderBase {
+ public struct WidgetData: WidgetInfo {
+ let ID: Int?
+ }
+
+ private let defaultDataHolder = PaginationDataHolder(
+ itemsPerPage: ActiveAppsWidget.Constants.MAX_ROWS_PER_PAGE
+ )
+
+ let widgetKind: String
+
+ init(widgetKind: String){
+ self.widgetKind = widgetKind
+ }
+
+ deinit{
+ // if this provider goes out of scope, clear all entries
+ PageInfoManager.shared.clearAll()
+ }
+
+ override func getUpdatedData(_ apps: [AppSnapshot], _ context: WidgetInfo?) -> [AppSnapshot] {
+ var apps = apps
+
+ // if simulator, get the 10 simulated entries based on first entry
+ #if targetEnvironment(simulator)
+ apps = getSimulatedData(apps: apps)
+ #endif
+
+ // always start with first page since defaultDataHolder is never updated
+ var currentPageApps = defaultDataHolder.currentPage(inItems: apps)
+
+ if let widgetInfo = context,
+ let widgetID = widgetInfo.ID {
+
+ var navEvent = getPageInfo(widgetID: widgetID)
+ if let event = navEvent,
+ let direction = event.direction
+ {
+ let dataHolder = event.dataHolder!
+ currentPageApps = dataHolder.currentPage(inItems: apps)
+
+ // process navigation request only if event wasn't consumed yet
+ if !event.consumed {
+ switch (direction){
+ case Direction.up:
+ currentPageApps = dataHolder.prevPage(inItems: apps, whenUnavailable: .current)!
+ case Direction.down:
+ currentPageApps = dataHolder.nextPage(inItems: apps, whenUnavailable: .current)!
+ }
+ // mark the event as consumed
+ // this prevents duplicate getUpdatedData() requests for same navigation event
+ navEvent!.consumed = true
+ }
+ }else{
+ // construct fresh/replace existing
+ navEvent = NavigationEvent(direction: nil, consumed: true, dataHolder: PaginationDataHolder(other: defaultDataHolder))
+ }
+ // put back the data
+ updatePageInfo(widgetID: widgetID, navEvent: navEvent)
+ }
+
+ return currentPageApps
+ }
+
+
+ private func getPageInfo(widgetID: Int) -> NavigationEvent?{
+ return PageInfoManager.shared.getPageInfo(forWidgetKind: widgetKind, forWidgetID: widgetID)
+ }
+
+ private func updatePageInfo(widgetID: Int, navEvent: NavigationEvent?) {
+ PageInfoManager.shared.setPageInfo(forWidgetKind: widgetKind, forWidgetID: widgetID, value: navEvent)
+ }
+}
+
+/// TimelineProvider for WidgetAppIntentConfiguration widget type
+@available(iOS 17, *)
+extension ActiveAppsTimelineProvider: AppIntentTimelineProvider {
+
+ typealias Intent = WidgetUpdateIntent
+
+ func snapshot(for intent: Intent, in context: Context) async -> AppsEntry {
+ // system retains the previously configured ID value and posts the same here
+ let widgetData = WidgetData(ID: intent.ID)
+
+ let bundleIDs = await super.fetchActiveAppBundleIDs()
+ let snapshot = await self.snapshot(for: bundleIDs, in: widgetData)
+
+ return snapshot
+ }
+
+ func timeline(for intent: Intent, in context: Context) async -> Timeline> {
+ // system retains the previously configured ID value and posts the same here
+ let widgetData = WidgetData(ID: intent.ID)
+
+ let bundleIDs = await self.fetchActiveAppBundleIDs()
+ let timeline = await self.timeline(for: bundleIDs, in: widgetData)
+
+ return timeline
+ }
+}
diff --git a/AltWidget/AppsTimelineProvider.swift b/AltWidget/Providers/AppsTimelineProvider.swift
similarity index 73%
rename from AltWidget/AppsTimelineProvider.swift
rename to AltWidget/Providers/AppsTimelineProvider.swift
index 08cf3dd7..d6fb9a90 100644
--- a/AltWidget/AppsTimelineProvider.swift
+++ b/AltWidget/Providers/AppsTimelineProvider.swift
@@ -11,53 +11,67 @@ import CoreData
import AltStoreCore
-struct AppsEntry: TimelineEntry
+struct AppsEntry: TimelineEntry
{
var date: Date
var relevance: TimelineEntryRelevance?
var apps: [AppSnapshot]
var isPlaceholder: Bool = false
+
+ var context: T?
+
}
-struct AppsTimelineProvider
+class AppsTimelineProviderBase
{
typealias Entry = AppsEntry
- func placeholder(in context: TimelineProviderContext) -> AppsEntry
+ func placeholder(in context: TimelineProviderContext) -> AppsEntry
{
return AppsEntry(date: Date(), apps: [], isPlaceholder: true)
}
- func snapshot(for appBundleIDs: [String]) async -> AppsEntry
+ func snapshot(for appBundleIDs: [String], in context: T? = nil) async -> AppsEntry
{
do
{
try await self.prepare()
- let apps = try await self.fetchApps(withBundleIDs: appBundleIDs)
+ var apps = try await self.fetchApps(withBundleIDs: appBundleIDs)
- let entry = AppsEntry(date: Date(), apps: apps)
+ apps = getUpdatedData(apps, context)
+
+ let entry = AppsEntry(date: Date(), apps: apps, context: context)
return entry
}
catch
{
print("Failed to prepare widget snapshot:", error)
- let entry = AppsEntry(date: Date(), apps: [])
+ let entry = AppsEntry(date: Date(), apps: [], context: context)
return entry
}
}
- func timeline(for appBundleIDs: [String]) async -> Timeline
+ func timeline(for appBundleIDs: [String], in context: T? = nil) async -> Timeline>
{
do
{
try await self.prepare()
- let apps = try await self.fetchApps(withBundleIDs: appBundleIDs)
+ var apps = try await self.fetchApps(withBundleIDs: appBundleIDs)
+
+ apps = getUpdatedData(apps, context)
+
+ var entries = self.makeEntries(for: apps, in: context)
+
+// #if targetEnvironment(simulator)
+// if let first = entries.first{
+// entries = [first]
+// }
+// #endif
- let entries = self.makeEntries(for: apps)
let timeline = Timeline(entries: entries, policy: .atEnd)
return timeline
}
@@ -65,21 +79,27 @@ struct AppsTimelineProvider
{
print("Failed to prepare widget timeline:", error)
- let entry = AppsEntry(date: Date(), apps: [])
+ let entry = AppsEntry(date: Date(), apps: [], context: context)
let timeline = Timeline(entries: [entry], policy: .atEnd)
return timeline
}
}
+
+ func getUpdatedData(_ apps: [AppSnapshot], _ context: T?) -> [AppSnapshot]{
+ // override in subclasses as required
+ return apps
+ }
}
-private extension AppsTimelineProvider
+extension AppsTimelineProviderBase
{
- func prepare() async throws
+
+ private func prepare() async throws
{
try await DatabaseManager.shared.start()
}
- func fetchApps(withBundleIDs bundleIDs: [String]) async throws -> [AppSnapshot]
+ private func fetchApps(withBundleIDs bundleIDs: [String]) async throws -> [AppSnapshot]
{
let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
let apps = try await context.performAsync {
@@ -99,7 +119,7 @@ private extension AppsTimelineProvider
return apps
}
- func makeEntries(for snapshots: [AppSnapshot]) -> [AppsEntry]
+ func makeEntries(for snapshots: [AppSnapshot], in context: T? = nil) -> [AppsEntry]
{
let sortedAppsByExpirationDate = snapshots.sorted { $0.expirationDate < $1.expirationDate }
guard let firstExpiringApp = sortedAppsByExpirationDate.first, let lastExpiringApp = sortedAppsByExpirationDate.last else { return [] }
@@ -108,16 +128,16 @@ private extension AppsTimelineProvider
let numberOfDays = lastExpiringApp.expirationDate.numberOfCalendarDays(since: currentDate)
// Generate a timeline consisting of one entry per day.
- var entries: [AppsEntry] = []
+ var entries: [AppsEntry] = []
switch numberOfDays
{
case ..<0:
- let entry = AppsEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 0.0), apps: snapshots)
+ let entry = AppsEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 0.0), apps: snapshots, context: context)
entries.append(entry)
case 0:
- let entry = AppsEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 1.0), apps: snapshots)
+ let entry = AppsEntry(date: currentDate, relevance: TimelineEntryRelevance(score: 1.0), apps: snapshots, context: context)
entries.append(entry)
default:
@@ -141,7 +161,7 @@ private extension AppsTimelineProvider
score = 0
}
- let entry = AppsEntry(date: entryDate, relevance: TimelineEntryRelevance(score: score), apps: snapshots)
+ let entry = AppsEntry(date: entryDate, relevance: TimelineEntryRelevance(score: score), apps: snapshots, context: context)
return entry
}
@@ -150,31 +170,8 @@ private extension AppsTimelineProvider
return entries
}
-}
-
-extension AppsTimelineProvider: TimelineProvider
-{
- func getSnapshot(in context: Context, completion: @escaping (AppsEntry) -> Void)
- {
- Task {
- let bundleIDs = await self.fetchActiveAppBundleIDs()
-
- let snapshot = await self.snapshot(for: bundleIDs)
- completion(snapshot)
- }
- }
- func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void)
- {
- Task {
- let bundleIDs = await self.fetchActiveAppBundleIDs()
-
- let timeline = await self.timeline(for: bundleIDs)
- completion(timeline)
- }
- }
-
- private func fetchActiveAppBundleIDs() async -> [String]
+ func fetchActiveAppBundleIDs() async -> [String]
{
do
{
@@ -201,26 +198,26 @@ extension AppsTimelineProvider: TimelineProvider
}
}
-extension AppsTimelineProvider: IntentTimelineProvider
+typealias Intent = ViewAppIntent
+
+class AppsTimelineProvider: AppsTimelineProviderBase, IntentTimelineProvider
{
- typealias Intent = ViewAppIntent
-
- func getSnapshot(for intent: Intent, in context: Context, completion: @escaping (AppsEntry) -> Void)
+ func getSnapshot(for intent: Intent, in context: Context, completion: @escaping (AppsEntry) -> Void)
{
Task {
let bundleIDs = [intent.app?.identifier ?? StoreApp.altstoreAppID]
- let snapshot = await self.snapshot(for: bundleIDs)
+ let snapshot = await self.snapshot(for: bundleIDs, in: intent)
completion(snapshot)
}
}
- func getTimeline(for intent: Intent, in context: Context, completion: @escaping (Timeline) -> Void)
+ func getTimeline(for intent: Intent, in context: Context, completion: @escaping (Timeline>) -> Void)
{
Task {
let bundleIDs = [intent.app?.identifier ?? StoreApp.altstoreAppID]
- let timeline = await self.timeline(for: bundleIDs)
+ let timeline = await self.timeline(for: bundleIDs, in: intent)
completion(timeline)
}
}
diff --git a/AltWidget/Widgets/ActiveAppsWidget.swift b/AltWidget/Widgets/ActiveAppsWidget.swift
index 412de6fc..9fd6dc4a 100644
--- a/AltWidget/Widgets/ActiveAppsWidget.swift
+++ b/AltWidget/Widgets/ActiveAppsWidget.swift
@@ -1,5 +1,5 @@
//
-// HomeScreenWidget.swift
+// ActiveAppsWidget.swift
// AltWidgetExtension
//
// Created by Riley Testut on 8/16/23.
@@ -8,10 +8,10 @@
import SwiftUI
import WidgetKit
-import CoreData
import AltStoreCore
-import AltSign
+
+import GameplayKit
private extension Color
{
@@ -21,20 +21,42 @@ private extension Color
static let altGradientExtraDark = Color.init(.displayP3, red: 2.0/255.0, green: 82.0/255.0, blue: 103.0/255.0)
}
+struct WidgetTag: WidgetInfo{
+ let ID: Int?
+}
+
//@available(iOS 17, *)
struct ActiveAppsWidget: Widget
{
- private let kind: String = "ActiveApps"
+ struct Constants{
+ static let MAX_ROWS_PER_PAGE: UInt = 3
+ }
+
+ private static var id: Int = 1
+ private let widgetKind: String
+
+ init(){
+ widgetKind = "ActiveApps - \(Self.id)"
+ Self.id += 1
+ }
public var body: some WidgetConfiguration {
+
if #available(iOS 17, *)
{
- return StaticConfiguration(kind: kind, provider: AppsTimelineProvider()) { entry in
- ActiveAppsWidgetView(entry: entry)
+
+ let widgetConfig = AppIntentConfiguration(
+ kind: widgetKind,
+ intent: WidgetUpdateIntent.self,
+ provider: ActiveAppsTimelineProvider(widgetKind: widgetKind)
+ ) { entry in
+ ActiveAppsWidgetView(entry: entry, widgetKind: widgetKind)
}
.supportedFamilies([.systemMedium])
.configurationDisplayName("Active Apps")
.description("View remaining days until your active apps expire. Tap the countdown timers to refresh them in the background.")
+
+ return widgetConfig
}
else
{
@@ -48,11 +70,12 @@ struct ActiveAppsWidget: Widget
@available(iOS 17, *)
private struct ActiveAppsWidgetView: View
{
- var entry: AppsEntry
+ var entry: AppsEntry
+ var widgetKind: String
@Environment(\.colorScheme)
private var colorScheme
-
+
var body: some View {
Group {
if entry.apps.isEmpty
@@ -79,24 +102,39 @@ private struct ActiveAppsWidgetView: View
private var content: some View {
GeometryReader { (geometry) in
-
- let numberOfApps = max(entry.apps.count, 1) // Ensure we don't divide by 0
- let preferredRowHeight = (geometry.size.height / Double(numberOfApps)) - 8
- let rowHeight = min(preferredRowHeight, geometry.size.height / 2)
-
- ZStack(alignment: .center) {
- VStack(spacing: 12) {
- ForEach(entry.apps, id: \.bundleIdentifier) { app in
+ HStack(alignment: .center) {
+
+ let itemsPerPage = ActiveAppsWidget.Constants.MAX_ROWS_PER_PAGE
+
+ let preferredRowHeight = (geometry.size.height / Double(itemsPerPage)) - 8
+ let rowHeight = min(preferredRowHeight, geometry.size.height / 2)
+
+ LazyVStack(spacing: 12) {
+ ForEach(Array(entry.apps.enumerated()), id: \.offset) { index, app in
+
+ let icon: UIImage = app.icon ?? UIImage(named: "SideStore")!
- let daysRemaining = app.expirationDate.numberOfCalendarDays(since: entry.date)
+ // 1024x1024 images are not supported by previews but supported by device
+ // so we scale the image to 97% so as to reduce its actual size but not too much
+ // to somewhere below value, acceptable by previews ie < 1042x948
+ let scalingFactor = 0.97
+
+ let resizedSize = CGSize(
+ width: icon.size.width * scalingFactor,
+ height: icon.size.height * scalingFactor
+ )
+
+ let resizedIcon = icon.resizing(to: resizedSize)!
let cornerRadius = rowHeight / 5.0
-
+ let daysRemaining = app.expirationDate.numberOfCalendarDays(since: entry.date)
+
HStack(spacing: 10) {
- Image(uiImage: app.icon ?? UIImage(named: "AltStore")!)
+ Image(uiImage: resizedIcon)
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(cornerRadius)
+
VStack(alignment: .leading, spacing: 1) {
Text(app.name)
.font(.system(size: 15, weight: .semibold, design: .rounded))
@@ -127,13 +165,40 @@ private struct ActiveAppsWidgetView: View
.padding(.all, -5)
}
.font(.system(size: 16, weight: .semibold, design: .rounded))
- .invalidatableContent()
- .padding(.horizontal, 8)
.activatesRefreshAllAppsIntent()
+ // this modifier invalidates the view (disables user interaction and shows a blinking effect)
+ .invalidatableContent()
+
}
.frame(height: rowHeight)
+
}
}
+
+ Spacer(minLength: 16)
+
+ let buttonWidth: CGFloat = 16
+ let widgetID = entry.context?.ID
+
+ VStack {
+ Image(systemName: "arrow.up")
+ .resizable()
+ .frame(width: buttonWidth, height: buttonWidth)
+ .opacity(0.3)
+ // .mask(Capsule())
+ .pageUpButton(widgetID, widgetKind)
+
+ Spacer()
+
+ Image(systemName: "arrow.down")
+ .resizable()
+ .frame(width: buttonWidth, height: buttonWidth)
+ .opacity(0.3)
+ // .mask(Capsule())
+ .pageDownButton(widgetID, widgetKind)
+ }
+ .padding(.vertical)
+
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
@@ -154,14 +219,14 @@ private struct ActiveAppsWidgetView: View
let expiredDate = Date().addingTimeInterval(1 * 60 * 60 * 24 * 7)
let (altstore, delta, clip, longAltStore, longDelta, longClip) = AppSnapshot.makePreviewSnapshots()
- AppsEntry(date: Date(), apps: [altstore, delta, clip])
- AppsEntry(date: Date(), apps: [longAltStore, longDelta, longClip])
+ AppsEntry(date: Date(), apps: [altstore, delta, clip])
+ AppsEntry(date: Date(), apps: [longAltStore, longDelta, longClip])
- AppsEntry(date: expiredDate, apps: [altstore, delta, clip])
+ AppsEntry(date: expiredDate, apps: [altstore, delta, clip])
- AppsEntry(date: Date(), apps: [altstore, delta])
- AppsEntry(date: Date(), apps: [altstore])
+ AppsEntry(date: Date(), apps: [altstore, delta])
+ AppsEntry(date: Date(), apps: [altstore])
- AppsEntry(date: Date(), apps: [])
- AppsEntry(date: Date(), apps: [], isPlaceholder: true)
+ AppsEntry(date: Date(), apps: [])
+ AppsEntry(date: Date(), apps: [], isPlaceholder: true)
}
diff --git a/AltWidget/Widgets/AppDetailWidget.swift b/AltWidget/Widgets/AppDetailWidget.swift
index b59c6a77..67cc0080 100644
--- a/AltWidget/Widgets/AppDetailWidget.swift
+++ b/AltWidget/Widgets/AppDetailWidget.swift
@@ -39,7 +39,7 @@ struct AppDetailWidget: Widget
private struct AppDetailWidgetView: View
{
- var entry: AppsEntry
+ var entry: AppsEntry
var body: some View {
Group {
@@ -130,7 +130,12 @@ private struct AppDetailWidgetView: View
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
- .widgetBackground(backgroundView(icon: entry.apps.first?.icon, tintColor: entry.apps.first?.tintColor))
+ .widgetBackground(
+ backgroundView(
+ icon: entry.apps.first?.icon,
+ tintColor: entry.apps.first?.tintColor
+ )
+ )
}
}
@@ -146,17 +151,35 @@ private extension AppDetailWidgetView
let blurRadius = 5 as CGFloat
let tintOpacity = 0.45
+ // 1024x1024 images are not supported by previews but supported by device
+ // so we scale the image to 97% so as to reduce its actual size but not too much
+ // to somewhere below value, acceptable by previews ie < 1042x948
+ let scalingFactor = 0.97
+
+ let resizedSize = CGSize(
+ width: icon.size.width * scalingFactor,
+ height: icon.size.height * scalingFactor
+ )
+
+ let resizedIcon = icon.resizing(to: resizedSize)!
+
return ZStack(alignment: .topTrailing) {
// Blurred Image
GeometryReader { geometry in
ZStack {
- Image(uiImage: icon)
+ Image(uiImage: resizedIcon)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: imageHeight, height: imageHeight, alignment: .center)
.saturation(saturation)
.blur(radius: blurRadius, opaque: true)
.scaleEffect(geometry.size.width / imageHeight, anchor: .center)
+ // .onAppear {
+ // print("Geometry size: \(geometry.size)")
+ // print("Image height: \(imageHeight), Geometry width: \(geometry.size.width)")
+ // print("Icon size: \(icon.size)")
+ // }
+
Color(tintColor)
.opacity(tintOpacity)
@@ -176,14 +199,12 @@ private extension AppDetailWidgetView
AppDetailWidget()
} timeline: {
let expiredDate = Date().addingTimeInterval(1 * 60 * 60 * 24 * 7)
- let (altstore, delta, _, _, longDelta, _) = AppSnapshot.makePreviewSnapshots()
+ let (altstore, _, _, longAltStore, _, _) = AppSnapshot.makePreviewSnapshots()
+ AppsEntry(date: Date(), apps: [altstore])
+ AppsEntry(date: Date(), apps: [longAltStore])
- AppsEntry(date: Date(), apps: [altstore])
- AppsEntry(date: Date(), apps: [delta])
- AppsEntry(date: Date(), apps: [longDelta])
+ AppsEntry(date: expiredDate, apps: [altstore])
- AppsEntry(date: expiredDate, apps: [delta])
-
- AppsEntry(date: Date(), apps: [])
- AppsEntry(date: Date(), apps: [], isPlaceholder: true)
+ AppsEntry(date: Date(), apps: [])
+ AppsEntry(date: Date(), apps: [], isPlaceholder: true)
}
diff --git a/AltWidget/Widgets/LockScreenWidget.swift b/AltWidget/Widgets/LockScreenWidget.swift
index 5cd35901..68cbea41 100644
--- a/AltWidget/Widgets/LockScreenWidget.swift
+++ b/AltWidget/Widgets/LockScreenWidget.swift
@@ -70,7 +70,7 @@ extension ComplicationView
@available(iOS 16, *)
private struct ComplicationView: View
{
- let entry: AppsEntry
+ let entry: AppsEntry
let style: Style
var body: some View {
@@ -82,6 +82,7 @@ private struct ComplicationView: View
let progress = Double(daysRemaining) / Double(totalDays)
+ // TODO: Gauge initialized with an out-of-bounds progress amount. The amount will be clamped to the nearest bound.
Gauge(value: progress) {
if daysRemaining < 0
{
@@ -143,10 +144,10 @@ private let widgetFamily = if #available(iOS 16, *) { WidgetFamily.accessoryCirc
let expiredDate = Date().addingTimeInterval(1 * 60 * 60 * 24 * 7)
let (altstore, _, _, longAltStore, _, _) = AppSnapshot.makePreviewSnapshots()
- AppsEntry(date: Date(), apps: [altstore])
- AppsEntry(date: Date(), apps: [longAltStore])
+ AppsEntry(date: Date(), apps: [altstore])
+ AppsEntry(date: Date(), apps: [longAltStore])
- AppsEntry(date: expiredDate, apps: [altstore])
+ AppsEntry(date: expiredDate, apps: [altstore])
}
@available(iOS 17, *)
@@ -156,8 +157,8 @@ private let widgetFamily = if #available(iOS 16, *) { WidgetFamily.accessoryCirc
let expiredDate = Date().addingTimeInterval(1 * 60 * 60 * 24 * 7)
let (altstore, _, _, longAltStore, _, _) = AppSnapshot.makePreviewSnapshots()
- AppsEntry(date: Date(), apps: [altstore])
- AppsEntry(date: Date(), apps: [longAltStore])
+ AppsEntry(date: Date(), apps: [altstore])
+ AppsEntry(date: Date(), apps: [longAltStore])
- AppsEntry(date: expiredDate, apps: [altstore])
+ AppsEntry(date: expiredDate, apps: [altstore])
}
diff --git a/Build.xcconfig b/Build.xcconfig
index a7f46a78..93135631 100644
--- a/Build.xcconfig
+++ b/Build.xcconfig
@@ -16,17 +16,17 @@ ORG_PREFIX = $(ORG_IDENTIFIER)
PRODUCT_NAME = SideStore
//PRODUCT_NAME[configuration=Debug] = Prov Debug
+
//PRODUCT_BUNDLE_IDENTIFIER[config=Debug] = $(ORG_PREFIX).SideStore$(BUNDLE_ID_SUFFIX)
//PRODUCT_BUNDLE_IDENTIFIER[config=Release] = $(ORG_PREFIX).SideStore
-PRODUCT_BUNDLE_IDENTIFIER = $(ORG_PREFIX).SideStore$(BUNDLE_ID_SUFFIX)
+// preserve unmodified bundle ID (without any extra suffixes)
+GROUP_ID = $(ORG_PREFIX).SideStore$(BUNDLE_ID_SUFFIX)
+PRODUCT_BUNDLE_IDENTIFIER = $(GROUP_ID)
EXTENSION_PREFIX = $(PRODUCT_BUNDLE_IDENTIFIER)
-APP_GROUP_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER)
+APP_GROUP_IDENTIFIER = $(GROUP_ID)
ICLOUD_CONTAINER_IDENTIFIER = iCloud.$(ORG_PREFIX).$(PROJECT_NAME)
-// preserve unmodified bundle ID (without any extra suffixes)
-GROUP_ID = $(PRODUCT_BUNDLE_IDENTIFIER)
-
// Suppress noise from os activity in xcode console log for release builds
DEBUG_ACTIVITY_MODE = disable
diff --git a/Makefile b/Makefile
index 1b8a530e..b6e14108 100755
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,5 @@
+default: build # default target for the "make" command
+
SHELL := /bin/bash
.PHONY: help ios update tvos
@@ -156,23 +158,32 @@ test:
## -- Building --
-# Fetch the latest commit ID globally
-ALPHA_COMMIT_ID := $(if $(IS_ALPHA),$(shell git rev-parse --short HEAD),)
+IS_ALPHA_TRUE := $(filter true TRUE 1, $(IS_ALPHA))
+IS_BETA_TRUE := $(filter true TRUE 1, $(IS_BETA))
-# Print release type based on the presence of ALPHA_COMMIT_ID
+# Fetch the latest commit ID for ALPHA or BETA builds
+COMMIT_ID := $(if $(or $(IS_ALPHA_TRUE),$(IS_BETA_TRUE)),$(shell git rev-parse --short HEAD),)
+
+# Print release type based on the value of IS_ALPHA or IS_BETA
print_release_type:
@echo ""
- @if [ -n "$(IS_ALPHA)" ]; then \
- echo "'IS_ALPHA' is defined. Fetched the latest commit ID from HEAD..."; \
- echo " Commit ID: $(ALPHA_COMMIT_ID)"; \
+ @if [ "$(filter true TRUE 1,$(IS_ALPHA))" ]; then \
+ echo "'IS_ALPHA' is set to true. Fetched the latest commit ID from HEAD..."; \
+ echo " Commit ID: $(COMMIT_ID)"; \
echo ""; \
- echo ">>>>>>>> This is now a ALPHA release for COMMIT_ID = '$(ALPHA_COMMIT_ID)' <<<<<<<<<"; \
- echo " Building with BUILD_REVISION = '$(ALPHA_COMMIT_ID)'"; \
+ echo ">>>>>>>> This is now an ALPHA release for COMMIT_ID = '$(COMMIT_ID)' <<<<<<<<<"; \
+ echo " Building with BUILD_REVISION = '$(COMMIT_ID)'"; \
+ elif [ "$(filter true TRUE 1,$(IS_BETA))" ]; then \
+ echo "'IS_BETA' is set to true. Fetched the latest commit ID from HEAD..."; \
+ echo " Commit ID: $(COMMIT_ID)"; \
+ echo ""; \
+ echo ">>>>>>>> This is now a BETA release for COMMIT_ID = '$(COMMIT_ID)' <<<<<<<<<"; \
+ echo " Building with BUILD_REVISION = '$(COMMIT_ID)'"; \
else \
- echo "'IS_ALPHA' is not defined. Skipping commit ID fetch."; \
+ echo "'IS_ALPHA' and 'IS_BETA' are not set to true. Skipping commit ID fetch."; \
echo ""; \
- echo ">>>>>>>> This is now a STABLE release because IS_ALPHA was NOT SET <<<<<<<<<"; \
- echo " Building with BUILD_REVISION = '$(ALPHA_COMMIT_ID)'"; \
+ echo ">>>>>>>> This is now a STABLE release because neither IS_ALPHA nor IS_BETA was true <<<<<<<<<"; \
+ echo " Building with BUILD_REVISION = '$(COMMIT_ID)'"; \
echo ""; \
fi
@echo ""
@@ -185,9 +196,14 @@ print_release_type:
#
# However the scheme used is Debug Scheme, so it was deliberately
# using scheme = Debug and config = Release (so I have kept it as-is)
-BUILD_CONFIG := "Debug" # switched to debug build-config to diagnose issue since debugger won't resolve breakpoints in release
+# BUILD_CONFIG := "Debug" # switched to debug build-config to diagnose issue since debugger won't resolve breakpoints in release
# BUILD_CONFIG := "Release"
+
+# switched back to release build as default config, unless specified by the incoming environment vars
+BUILD_CONFIG ?= Release
build: print_release_type
+ @echo ">>>>>>>> BUILD_CONFIG is set to '$(BUILD_CONFIG)', Building for $(BUILD_CONFIG) mode! <<<<<<<<<"
+ @echo ""
@xcodebuild -workspace AltStore.xcworkspace \
-scheme SideStore \
-sdk iphoneos \
@@ -198,7 +214,7 @@ build: print_release_type
CODE_SIGNING_ALLOWED=NO \
DEVELOPMENT_TEAM=XYZ0123456 \
ORG_IDENTIFIER=com.SideStore \
- BUILD_REVISION=$(ALPHA_COMMIT_ID) \
+ BUILD_REVISION=$(COMMIT_ID) \
BUNDLE_ID_SUFFIX=
# DWARF_DSYM_FOLDER_PATH="."
@@ -302,13 +318,13 @@ copy-altbackup: checkPaths
else \
rm -rf "$$TGT"; \
mkdir -p "$$TGT"; \
- cp -R "$(ALT_APP_SRC_PARENT)/$(TGT_NAME)" "$$TGT"; \
+ cp -R -f "$(ALT_APP_SRC_PARENT)/$$TGT_NAME/." "$$TGT"; \
echo " Copied $$TGT_NAME into TARGET = $$TGT"; \
echo ""; \
fi; \
done \
'
- @find "$(ALT_APP_DST_ARCHIVE)" -maxdepth 3 -exec ls -ld {} + || true
+ @find "$(ALT_APP_DST_ARCHIVE)" -maxdepth 4 -exec ls -ld {} + || true
@echo ''
# fakesign-altbackup: copy-altbackup
@@ -325,12 +341,19 @@ ipa-altbackup: checkPaths copy-altbackup
@rm -rf "$(ALT_APP_PAYLOAD_DST)"
@mkdir -p "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
@echo " Copying from $(ALT_APP_SRC) into $(ALT_APP_PAYLOAD_DST)"
- @cp -R -f "$(ALT_APP_SRC)/" "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
+ @cp -R -f "$(ALT_APP_SRC)/." "$(ALT_APP_PAYLOAD_DST)/$(TARGET_NAME)"
@pushd "$(ALT_APP_DST_ARCHIVE)" && zip -r "../../$(ALT_APP_IPA_DST)" Payload && popd
@cp -f "$(ALT_APP_IPA_DST)" AltStore/Resources
@echo " IPA created: AltStore/Resources/AltBackup.ipa"
-clean:
+clean-altbackup:
+ @echo ""
+ @echo "====> Cleaning up AltBackup related artifacts <===="
+ @rm -rf build/altbackup.xcarchive/
+ @rm -f build/AltBackup.ipa
+ @rm -f AltStore/Resources/AltBackup.ipa
+
+clean: clean-altbackup
@rm -rf *.xcarchive/
@rm -rf *.dSYM/
@rm -rf SideStore.ipa
diff --git a/README.md b/README.md
index db8f16fb..acbc94e6 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ SideStore's goal is to provide an untethered sideloading experience. It's a comm
- iOS 15+
- Rustup (`brew install rustup`)
-Why iOS 14? Targeting such a recent version of iOS allows us to accelerate development, especially since not many developers have older devices to test on. This is corrobated by the fact that SwiftUI support is much better, allowing us to transistion to a more modern UI codebase.
+Why iOS 15? Targeting such a recent version of iOS allows us to accelerate development, especially since not many developers have older devices to test on. This is corrobated by the fact that SwiftUI support is much better, allowing us to transistion to a more modern UI codebase.
## Project Overview
### SideStore
diff --git a/SideStore/Utils/common/AbstractClassError.swift b/SideStore/Utils/common/AbstractClassError.swift
new file mode 100644
index 00000000..15d3004e
--- /dev/null
+++ b/SideStore/Utils/common/AbstractClassError.swift
@@ -0,0 +1,12 @@
+//
+// OutputStream.swift
+// AltStore
+//
+// Created by Magesh K on 28/12/24.
+// Copyright © 2024 SideStore. All rights reserved.
+//
+
+public enum AbstractClassError: Error {
+ case abstractInitializerInvoked
+ case abstractMethodInvoked
+}
diff --git a/SideStore/Utils/common/DateTimeUtil.swift b/SideStore/Utils/common/DateTimeUtil.swift
new file mode 100644
index 00000000..f07bb761
--- /dev/null
+++ b/SideStore/Utils/common/DateTimeUtil.swift
@@ -0,0 +1,26 @@
+//
+// DateTimeUtil.swift
+// AltStore
+//
+// Created by Magesh K on 02/01/25.
+// Copyright © 2025 SideStore. All rights reserved.
+//
+
+import Foundation
+
+public class DateTimeUtil {
+ public static func getDateInTimeStamp(date: Date) -> String {
+ let formatter = DateFormatter()
+ // (upto millis accurate for uniqueness)
+ formatter.dateFormat = "yyyyMMdd_HHmmss_SSS" // Format: 20241228_142345_300
+ // Ensures 24-hour clock format coz the locale value overrides it if it is of AM/PM format?! (why apple!)
+ formatter.locale = Locale(identifier: "en_US_POSIX")
+ return formatter.string(from: date)
+ }
+
+ public static func getTimeStampSuffixedFileName(fileName: String, timestamp: String, extn: String) -> String {
+ // create a log file with the current timestamp
+ let fnameWithTimestamp = "\(fileName)-\(timestamp)\(extn)"
+ return fnameWithTimestamp
+ }
+}
diff --git a/SideStore/Utils/common/FileOutputStream.swift b/SideStore/Utils/common/FileOutputStream.swift
new file mode 100644
index 00000000..fdee0a2f
--- /dev/null
+++ b/SideStore/Utils/common/FileOutputStream.swift
@@ -0,0 +1,29 @@
+//
+// FileOutputStream.swift
+// AltStore
+//
+// Created by Magesh K on 28/12/24.
+// Copyright © 2024 SideStore. All rights reserved.
+//
+
+import Foundation
+
+public class FileOutputStream: OutputStream {
+ private let fileHandle: FileHandle
+
+ init(_ fileHandle: FileHandle) {
+ self.fileHandle = fileHandle
+ }
+
+ public func write(_ data: Data) {
+ fileHandle.write(data)
+ }
+
+ public func flush() {
+ fileHandle.synchronizeFile()
+ }
+
+ public func close() {
+ fileHandle.closeFile()
+ }
+}
diff --git a/SideStore/Utils/common/OutputStream.swift b/SideStore/Utils/common/OutputStream.swift
new file mode 100644
index 00000000..ddc421ca
--- /dev/null
+++ b/SideStore/Utils/common/OutputStream.swift
@@ -0,0 +1,15 @@
+//
+// OutputStream.swift
+// AltStore
+//
+// Created by Magesh K on 28/12/24.
+// Copyright © 2024 SideStore. All rights reserved.
+//
+
+import Foundation
+
+public protocol OutputStream {
+ func write(_ data: Data)
+ func flush()
+ func close()
+}
diff --git a/SideStore/Utils/datastructures/SingletonGenericMap.swift b/SideStore/Utils/datastructures/SingletonGenericMap.swift
new file mode 100644
index 00000000..067e8a0d
--- /dev/null
+++ b/SideStore/Utils/datastructures/SingletonGenericMap.swift
@@ -0,0 +1,30 @@
+//
+// SingletonGenericMap.swift
+// SideStore
+//
+// Created by Magesh K on 10/01/25.
+// Copyright © 2025 SideStore. All rights reserved.
+//
+
+class SingletonGenericMap{
+ static var shared = SingletonGenericMap()
+ private var pageInfoMap: [AnyHashable: Any] = [:]
+
+ private init() {}
+
+ func setPageInfo(for key: T, value: U?) {
+ pageInfoMap[key] = value
+ }
+
+ func getPageInfo(for key: T) -> U? {
+ return pageInfoMap[key] as? U
+ }
+
+ func popPageInfo(for key: T) -> U? {
+ return pageInfoMap.removeValue(forKey: key) as? U
+ }
+
+ func clearAll() {
+ pageInfoMap.removeAll()
+ }
+}
diff --git a/SideStore/Utils/dignostics/database/CoreDataHelper.swift b/SideStore/Utils/dignostics/database/CoreDataHelper.swift
new file mode 100644
index 00000000..75dbad6b
--- /dev/null
+++ b/SideStore/Utils/dignostics/database/CoreDataHelper.swift
@@ -0,0 +1,169 @@
+//
+// CoreDataHelper.swift
+// AltStore
+//
+// Created by Magesh K on 02/01/25.
+// Copyright © 2025 SideStore. All rights reserved.
+//
+
+
+import Foundation
+import CoreData
+import System
+
+class CoreDataHelper{
+
+ private static let STORE_XCMODELD_NAME = "AltStore"
+ private static let COREDATA_BUNDLE_ID = "com.SideStore.SideStore.AltStoreCore"
+
+ // Create a serial dispatch queue to lock access to the Core Data store
+ private static let datastoreQueue = DispatchQueue(label: "com.SideStore.AltStore.datastoreQueue")
+
+ public static func exportCoreDataStore() async throws -> URL {
+
+ // Locate the bundle containing the Core Data model
+ guard let bundle = Bundle(identifier: COREDATA_BUNDLE_ID) else {
+ let errorDescription = "AltStoreCore bundle not found"
+ throw getCoreDataError(code: 1, localizedDescription: errorDescription)
+ }
+
+ // Load the model from the bundle
+ guard let modelURL = bundle.url(forResource: STORE_XCMODELD_NAME, withExtension: "momd"),
+ let model = NSManagedObjectModel(contentsOf: modelURL) else {
+
+ let errorDescription = "Failed to load model \(STORE_XCMODELD_NAME) from AltStoreCore bundle"
+ throw getCoreDataError(code: 2, localizedDescription: errorDescription)
+ }
+
+// let container = NSPersistentContainer(name: STORE_XCMODELD_NAME)
+ let container = NSPersistentContainer(name: STORE_XCMODELD_NAME, managedObjectModel: model)
+
+
+ // bridge callback into async-await pattern
+ return try await withCheckedThrowingContinuation{ (continuation: CheckedContinuation) in
+
+ // async callback processing
+ container.loadPersistentStores { description, error in
+ // perform actual backup in sync manner
+ do{
+ let exportedURL = try backupCoreDataStore(container: container, loadError: error)
+ continuation.resume(returning: exportedURL)
+ }catch{
+ continuation.resume(throwing: error)
+ }
+ }
+ }
+ }
+
+ private static func lockSQLiteFile(at url: URL) -> FileDescriptor? {
+ // Open the SQLite file for locking
+ let fileDescriptor = open(url.path, O_RDWR)
+ guard fileDescriptor >= 0 else {
+ print("Failed to open SQLite file for locking.")
+ return nil
+ }
+
+ // Lock the file using flock (exclusive lock)
+ let lockResult = flock(fileDescriptor, LOCK_EX)
+ guard lockResult == 0 else {
+ print("Failed to lock SQLite file.")
+ close(fileDescriptor)
+ return nil
+ }
+
+ return FileDescriptor(rawValue: fileDescriptor)
+ }
+
+ private static func unlockSQLiteFile(fileDescriptor: FileDescriptor) {
+ let fileDescriptor = fileDescriptor.rawValue
+ // Unlock the file after backup
+ flock(fileDescriptor, LOCK_UN)
+ close(fileDescriptor)
+ }
+
+ private static func getCoreDataError(code: Int, localizedDescription: String) -> Error {
+ return NSError(domain: "CoreDataExport", code: code, userInfo: [NSLocalizedDescriptionKey: localizedDescription])
+ }
+
+
+ private static func backupCoreDataStore(container: NSPersistentContainer, loadError: Error?) throws -> URL {
+
+ // Check for load errors
+ if let error = loadError {
+ let errorDescription = "Failed to load persistent store: \(error.localizedDescription)"
+ throw getCoreDataError(code: 3, localizedDescription: errorDescription)
+ }
+
+ guard let storeURL = container.persistentStoreCoordinator.persistentStores.first?.url else {
+ let errorDescription = "Persistent store URL not found"
+ throw getCoreDataError(code: 4, localizedDescription: errorDescription)
+ }
+
+ // TODO: we can't lock on the sqlite file for serialization coz coredata might be holding
+ // active database connection handle to the sqlite
+
+// // Lock the SQLite file
+// guard let fileDescriptor = lockSQLiteFile(at: storeURL) else {
+// throw getCoreDataError(code: 5, localizedDescription: "Failed to lock SQLite file")
+// }
+//
+// defer {
+// // Ensure that the file is unlocked when the backup completes or fails
+// unlockSQLiteFile(fileDescriptor: fileDescriptor)
+// }
+
+ let fileManager = FileManager.default
+ let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
+ let exportedDir = documentsURL.appendingPathComponent("ExportedCoreDataStores", isDirectory: true)
+
+ let currentDateTime = Date()
+ let currentTimeStamp = DateTimeUtil.getDateInTimeStamp(date: currentDateTime)
+
+ let fileNamePrefix = storeURL.deletingPathExtension().lastPathComponent
+ let fileExtension = storeURL.pathExtension
+ let fileName = DateTimeUtil.getTimeStampSuffixedFileName(
+ fileName: fileNamePrefix,
+ timestamp: currentTimeStamp,
+ extn: "." + fileExtension
+ )
+
+ let destinationURL = exportedDir.appendingPathComponent(fileName)
+
+ let directoryURL = storeURL.deletingLastPathComponent()
+ if let files = try? FileManager.default.contentsOfDirectory(atPath: directoryURL.path) {
+ print("Files in Application Support: \(files)")
+ } else {
+ print("Failed to list directory contents.")
+ }
+
+ let parentDirectory = destinationURL.deletingLastPathComponent()
+
+
+ do {
+ // create intermediate dirs as required
+ try FileManager.default.createDirectory(at: parentDirectory,
+ withIntermediateDirectories: true,
+ attributes: nil)
+
+ // Copy main SQLite file
+ try fileManager.copyItem(at: storeURL, to: destinationURL)
+
+ // Copy -shm and -wal files if they exist
+ let additionalFiles = ["-shm", "-wal"].compactMap {
+ destinationURL.deletingPathExtension().appendingPathExtension(destinationURL.pathExtension + $0)
+ }
+
+ for file in additionalFiles where fileManager.fileExists(atPath: file.path) {
+ let destination = documentsURL.appendingPathComponent(file.lastPathComponent)
+ try fileManager.copyItem(at: file, to: destination)
+ }
+
+ print("Core Data store exported to: \(destinationURL.path)")
+ return destinationURL
+
+ } catch {
+ let errorDescription = "Failed to copy Core Data files: \(error.localizedDescription)"
+ throw getCoreDataError(code: 6, localizedDescription: errorDescription)
+ }
+ }
+}
diff --git a/SideStore/Utils/dignostics/errors/ErrorProcessing.swift b/SideStore/Utils/dignostics/errors/ErrorProcessing.swift
new file mode 100644
index 00000000..15489f58
--- /dev/null
+++ b/SideStore/Utils/dignostics/errors/ErrorProcessing.swift
@@ -0,0 +1,81 @@
+//
+// ErrorProcessing.swift
+// AltStore
+//
+// Created by Magesh K on 20/01/25.
+// Copyright © 2025 SideStore. All rights reserved.
+//
+
+class ErrorProcessing {
+
+ enum InfoMode: String {
+ case fullError
+ case localizedDescription
+ }
+
+ let info: InfoMode
+ let unique: Bool
+ let recur: Bool
+
+
+ var errors: Set = []
+
+ // by default we will process only the localDesc on first level errors
+ init(_ mode: InfoMode = .localizedDescription, unique: Bool = false, recur: Bool = false){
+ self.info = mode
+ self.unique = unique
+ self.recur = recur
+ }
+
+ private func processError(_ error: NSError, getMoreErrors: (_ error: NSError)->String) -> String{
+ // if unique was requested and if this error is duplicate, ignore processing it
+ let serializedError = "\(error)"
+ if unique && errors.contains(serializedError) {
+ return ""
+ }
+ errors.insert(serializedError) // mark this as processed
+
+ var title = ""
+ var desc = ""
+ switch (info){
+ case .localizedDescription:
+ title = (error.localizedTitle.map{$0+"\n"} ?? "")
+ desc = error.localizedDescription
+ case .fullError:
+ desc = serializedError
+ }
+ var moreErrors = getMoreErrors(error)
+ moreErrors = moreErrors == "" ? "" : "\n" + moreErrors
+ return title + desc + moreErrors
+ }
+
+ func getDescription(error: NSError) -> String{
+ errors = [] // reinit for each request
+ return getDescriptionText(error: error)
+ }
+
+ private lazy var recurseErrors = { error in
+ self.getDescriptionText(error: error) // recursively process underlying error(s) if any
+ }
+
+ func getDescriptionText(error: NSError) -> String{
+ var description = ""
+
+ // process current error only if recur was not requested
+ let processMoreErrors = recur ? recurseErrors : {_ in ""}
+
+ let underlyingErrors = error.underlyingErrors
+ if !underlyingErrors.isEmpty {
+ description += underlyingErrors.map{ error in
+ let error = error as NSError
+ return processError(error, getMoreErrors: processMoreErrors)
+ }.joined(separator: "\n")
+ } else if let underlyingError = error.underlyingError as? NSError {
+ let error = underlyingError as NSError
+ description += processError(error, getMoreErrors: processMoreErrors)
+ } else {
+ description += processError(error, getMoreErrors: processMoreErrors)
+ }
+ return description
+ }
+}
diff --git a/SideStore/Utils/dignostics/operations/OperationsLoggingControl.swift b/SideStore/Utils/dignostics/operations/OperationsLoggingControl.swift
new file mode 100644
index 00000000..0e1b4b22
--- /dev/null
+++ b/SideStore/Utils/dignostics/operations/OperationsLoggingControl.swift
@@ -0,0 +1,57 @@
+//
+// OperationsLoggingControl.swift
+// AltStore
+//
+// Created by Magesh K on 14/01/25.
+// Copyright © 2025 SideStore. All rights reserved.
+//
+
+import Foundation
+
+class OperationsLoggingControl {
+
+ func updateDatabase(for operation: Operation.Type, value: Bool) {
+ Self.updateDatabase(for: operation, value: value)
+ }
+
+ private static func updateDatabase(for operation: Operation.Type, value: Bool) {
+ // This method should handle the database update logic based on the operation and value
+ let key = Self.getKey(operation)
+ print("Updating database for key: \(key), value: \(value)")
+ UserDefaults.standard.set(value, forKey: key)
+ }
+
+ private static func stripGenericTypeName(from string: String) -> String {
+ // ex: 1. "EnableJITOperation"
+ // ex: 1. "EnableJITOperation>"
+ // will become EnableJITOperation without the generics type info
+ if let range = string.range(of: "<") {
+ return String(string[.. String {
+ let processedOperation = Self.stripGenericTypeName(from: "\(operation)")
+ return "\(processedOperation)LoggingEnabled"
+ }
+
+ func getFromDatabase(for operation: Operation.Type) -> Bool{
+ return Self.getFromDatabase(for: operation)
+ }
+
+ static func getUpdatedFromDatabase(for operation: Operation.Type, defaultVal: Bool) -> Bool{
+ let key = Self.getKey(operation)
+ let valueInDb = UserDefaults.standard.value(forKey: key) as? Bool
+ if valueInDb == nil {
+ // put the value if not already present
+ updateDatabase(for: operation, value: defaultVal)
+ }
+ return valueInDb ?? defaultVal
+ }
+
+ public static func getFromDatabase(for operation: Operation.Type) -> Bool {
+ let key = Self.getKey(operation)
+ return UserDefaults.standard.bool(forKey: key)
+ }
+}
diff --git a/SideStore/Utils/importexport/ImportExport.swift b/SideStore/Utils/importexport/ImportExport.swift
new file mode 100644
index 00000000..20da033e
--- /dev/null
+++ b/SideStore/Utils/importexport/ImportExport.swift
@@ -0,0 +1,135 @@
+//
+// ImportExport.swift
+// AltStore
+//
+// Created by Magesh K on 07/01/25.
+// Copyright © 2025 SideStore. All rights reserved.
+//
+
+
+import UIKit
+import AltStoreCore
+
+class ImportExport {
+
+ private static var documentPickerHandler: DocumentPickerHandler?
+
+ public static func getPreviousBackupURL(_ backupURL: URL) -> URL {
+ let backupParentDirectory = backupURL.deletingLastPathComponent()
+ let backupName = backupURL.lastPathComponent
+ let backupBakURL = backupParentDirectory.appendingPathComponent("\(backupName).bak")
+ return backupBakURL
+ }
+
+ /// Renames the existing backup contents at `backupURL` to `.bak`.
+ private static func renameBackupContents(at backupURL: URL) throws {
+
+ // rename backup to backup.bak dir only if backup dir exists
+ guard FileManager.default.fileExists(atPath: backupURL.path) else { return }
+
+ let backupBakURL = getPreviousBackupURL(backupURL)
+
+ let fileManager = FileManager.default
+ if fileManager.fileExists(atPath: backupBakURL.path) {
+ try fileManager.removeItem(at: backupBakURL) // Remove any existing .bak directory
+ }
+
+ try fileManager.moveItem(at: backupURL, to: backupBakURL)
+ }
+
+ /// Handles importing new backup data into the designated backup directory.
+ private static func importBackupContents(from documentPickerURL: URL, to backupURL: URL) throws {
+ let fileManager = FileManager.default
+
+ // Ensure the backup directory exists.
+ if !fileManager.fileExists(atPath: backupURL.path) {
+ try fileManager.createDirectory(at: backupURL, withIntermediateDirectories: true, attributes: nil)
+ }
+
+ print("Backup URL: \(backupURL)")
+ print("Document Picker URL: \(documentPickerURL)")
+
+ // Enumerate the contents of the selected directory and copy them to the backup directory.
+ let selectedContents = try fileManager.contentsOfDirectory(
+ at: documentPickerURL,
+ includingPropertiesForKeys: nil,
+ options: .skipsHiddenFiles
+ )
+ for itemURL in selectedContents {
+ let destinationURL = backupURL.appendingPathComponent(itemURL.lastPathComponent)
+
+ // Remove the existing file if it exists at the destination.
+ if fileManager.fileExists(atPath: destinationURL.path) {
+ try fileManager.removeItem(at: destinationURL)
+ }
+
+ // Copy the item.
+ try fileManager.copyItem(at: itemURL, to: destinationURL)
+ }
+ }
+
+ public static func importBackup(presentingViewController: UIViewController,
+ for installedApp: InstalledApp,
+ completionHandler: @escaping (Result) -> Void){
+ guard let backupURL = FileManager.default.backupDirectoryURL(for: installedApp) else {
+ return completionHandler(.failure(OperationError.invalidParameters("Error: Backup directory URL not found.")))
+ }
+
+ let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder], asCopy: false)
+ documentPicker.allowsMultipleSelection = false
+
+ // Create a handler and set it as the delegate
+ Self.documentPickerHandler = DocumentPickerHandler { selectedURL in
+ guard let selectedURL = selectedURL else {
+ return completionHandler(.failure( OperationError.cancelled))
+ }
+
+ // resolve symlinks if any, so that prefix match works
+ let appUserDataDir = FileManager.default.documentsDirectory.resolvingSymlinksInPath()
+ guard selectedURL.resolvingSymlinksInPath().path.hasPrefix(appUserDataDir.path) else {
+ return completionHandler(.failure(
+ OperationError.forbidden(failureReason: "Selected backup data directory is not within the app's user data directory"))
+ )
+ }
+
+ do {
+ // Rename existing backup contents to `.bak`.
+ try Self.renameBackupContents(at: backupURL)
+
+ // Import the contents of the selected folder into the backup directory.
+ try Self.importBackupContents(from: selectedURL, to: backupURL)
+
+ print("Backup imported successfully to:", backupURL.path)
+ return completionHandler(.success(()))
+ } catch {
+ print("Backup Error:", error)
+ return completionHandler(.failure( OperationError.invalidParameters(error.localizedDescription)))
+ }
+ }
+
+ documentPicker.delegate = Self.documentPickerHandler
+ // Present the picker
+ presentingViewController.present(documentPicker, animated: true, completion: nil)
+ }
+}
+
+private struct AssociatedKeys {
+ static var documentPickerHandler = "documentPickerHandler"
+}
+
+
+class DocumentPickerHandler: NSObject, UIDocumentPickerDelegate {
+ private let completion: (URL?) -> Void
+
+ init(completion: @escaping (URL?) -> Void) {
+ self.completion = completion
+ }
+
+ func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
+ completion(urls.first)
+ }
+
+ func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
+ completion(nil)
+ }
+}
diff --git a/SideStore/Utils/iostreams/ConsoleLog.swift b/SideStore/Utils/iostreams/ConsoleLog.swift
new file mode 100644
index 00000000..b4ff8e23
--- /dev/null
+++ b/SideStore/Utils/iostreams/ConsoleLog.swift
@@ -0,0 +1,71 @@
+//
+// ConsoleLog.swift
+// AltStore
+//
+// Created by Magesh K on 25/11/24.
+// Copyright © 2024 SideStore. All rights reserved.
+//
+//
+
+import Foundation
+
+class ConsoleLog {
+ private static let CONSOLE_LOGS_DIRECTORY = "ConsoleLogs"
+ private static let CONSOLE_LOG_NAME_PREFIX = "console"
+ private static let CONSOLE_LOG_EXTN = ".log"
+
+ private lazy var consoleLogger: ConsoleLogger = {
+ let logFileHandle = createLogFileHandle()
+ let fileOutputStream = FileOutputStream(logFileHandle)
+
+ return UnBufferedConsoleLogger(stream: fileOutputStream)
+ }()
+
+ private lazy var consoleLogsDir: URL = {
+ // create a directory for console logs
+ let docsDir = FileManager.default.documentsDirectory
+ let consoleLogsDir = docsDir.appendingPathComponent(ConsoleLog.CONSOLE_LOGS_DIRECTORY)
+ if !FileManager.default.fileExists(atPath: consoleLogsDir.path) {
+ try! FileManager.default.createDirectory(at: consoleLogsDir, withIntermediateDirectories: true, attributes: nil)
+ }
+ return consoleLogsDir
+ }()
+
+ public lazy var logName: String = {
+ logFileURL.lastPathComponent
+ }()
+
+ public lazy var logFileURL: URL = {
+ // get current timestamp
+ let currentTime = Date()
+ let dateTimeStamp = DateTimeUtil.getDateInTimeStamp(date: currentTime)
+
+ // create a log file with the current timestamp
+ let logName = DateTimeUtil.getTimeStampSuffixedFileName(
+ fileName: ConsoleLog.CONSOLE_LOG_NAME_PREFIX,
+ timestamp: dateTimeStamp,
+ extn: ConsoleLog.CONSOLE_LOG_EXTN
+ )
+ let logFileURL = consoleLogsDir.appendingPathComponent(logName)
+ return logFileURL
+ }()
+
+
+ private func createLogFileHandle() -> FileHandle {
+ if !FileManager.default.fileExists(atPath: logFileURL.path) {
+ FileManager.default.createFile(atPath: logFileURL.path, contents: nil, attributes: nil)
+ }
+
+ // return the file handle
+ return try! FileHandle(forWritingTo: logFileURL)
+ }
+
+ func startCapturing() {
+ consoleLogger.startCapturing()
+ }
+
+ func stopCapturing() {
+ consoleLogger.stopCapturing()
+ }
+}
+
diff --git a/SideStore/Utils/iostreams/ConsoleLogger.swift b/SideStore/Utils/iostreams/ConsoleLogger.swift
new file mode 100644
index 00000000..479cc5c8
--- /dev/null
+++ b/SideStore/Utils/iostreams/ConsoleLogger.swift
@@ -0,0 +1,166 @@
+//
+// ConsoleCapture.swift
+// AltStore
+//
+// Created by Magesh K on 25/11/24.
+// Copyright © 2024 SideStore. All rights reserved.
+//
+
+import Foundation
+
+protocol ConsoleLogger{
+ func startCapturing()
+ func stopCapturing()
+}
+
+public class AbstractConsoleLogger: ConsoleLogger{
+ var outPipe: Pipe?
+ var errPipe: Pipe?
+
+ var outputHandle: FileHandle?
+ var errorHandle: FileHandle?
+
+ var originalStdout: Int32?
+ var originalStderr: Int32?
+
+ let ostream: T
+
+ let writeQueue = DispatchQueue(label: "async-write-queue")
+
+ public init(stream: T) throws {
+ // Since swift doesn't support compile time abstract classes Instantiation checking,
+ // we are using runtime check to prevent direct instantiation :(
+ if Self.self === AbstractConsoleLogger.self {
+ throw AbstractClassError.abstractInitializerInvoked
+ }
+
+ self.ostream = stream
+ }
+
+ deinit {
+ stopCapturing()
+ }
+
+ public func startCapturing() { // made it public coz, let client ask for capturing
+
+ // if already initialized within current instance, bail out
+ guard outPipe == nil, errPipe == nil else {
+ return
+ }
+
+ // Create new pipes for stdout and stderr
+ self.outPipe = Pipe()
+ self.errPipe = Pipe()
+
+ outputHandle = self.outPipe?.fileHandleForReading
+ errorHandle = self.errPipe?.fileHandleForReading
+
+ // Store original file descriptors
+ originalStdout = dup(STDOUT_FILENO)
+ originalStderr = dup(STDERR_FILENO)
+
+ // Redirect stdout and stderr to our pipes
+ dup2(self.outPipe?.fileHandleForWriting.fileDescriptor ?? -1, STDOUT_FILENO)
+ dup2(self.errPipe?.fileHandleForWriting.fileDescriptor ?? -1, STDERR_FILENO)
+
+ // Setup readability handlers for raw data
+ setupReadabilityHandler(for: outputHandle, isError: false)
+ setupReadabilityHandler(for: errorHandle, isError: true)
+ }
+
+ private func setupReadabilityHandler(for handle: FileHandle?, isError: Bool) {
+ handle?.readabilityHandler = { [weak self] handle in
+ let data = handle.availableData
+ if !data.isEmpty {
+ self?.writeQueue.async {
+ try? self?.writeData(data)
+ }
+
+ // Forward to original std stream
+ if let originalFD = isError ? self?.originalStderr : self?.originalStdout {
+ data.withUnsafeBytes { (bufferPointer) -> Void in
+ if let baseAddress = bufferPointer.baseAddress, bufferPointer.count > 0 {
+ write(originalFD, baseAddress, bufferPointer.count)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ func writeData(_ data: Data) throws {
+ throw AbstractClassError.abstractMethodInvoked
+ }
+
+ func stopCapturing() {
+ ostream.close()
+
+ // Restore original stdout and stderr
+ if let stdout = originalStdout {
+ dup2(stdout, STDOUT_FILENO)
+ close(stdout)
+ }
+ if let stderr = originalStderr {
+ dup2(stderr, STDERR_FILENO)
+ close(stderr)
+ }
+
+ // Clean up
+ outPipe?.fileHandleForReading.readabilityHandler = nil
+ errPipe?.fileHandleForReading.readabilityHandler = nil
+ outPipe = nil
+ errPipe = nil
+ outputHandle = nil
+ errorHandle = nil
+ originalStdout = nil
+ originalStderr = nil
+ }
+}
+
+
+public class UnBufferedConsoleLogger: AbstractConsoleLogger {
+
+ required override init(stream: T) {
+ // cannot throw abstractInitializerInvoked, so need to override else client needs to handle it unnecessarily
+ try! super.init(stream: stream)
+ }
+
+ override func writeData(_ data: Data) throws {
+ // directly write data to the stream without buffering
+ ostream.write(data)
+ }
+}
+
+public class BufferedConsoleLogger: AbstractConsoleLogger {
+
+ // Buffer size (bytes) and storage
+ private let maxBufferSize: Int
+ private var bufferedData = Data()
+
+ required init(stream: T, bufferSize: Int = 1024) {
+ self.maxBufferSize = bufferSize
+ try! super.init(stream: stream)
+ }
+
+ override func writeData(_ data: Data) throws {
+ // Append data to buffer
+ self.bufferedData.append(data)
+
+ // Check if the buffer is full and flush
+ if self.bufferedData.count >= self.maxBufferSize {
+ self.flushBuffer()
+ }
+ }
+
+ private func flushBuffer() {
+ // Write all buffered data to the stream
+ ostream.write(bufferedData)
+ bufferedData.removeAll()
+ }
+
+ override func stopCapturing() {
+ // Flush buffer and close the file handles first
+ flushBuffer()
+ super.stopCapturing()
+ }
+}
diff --git a/SideStore/Utils/pagination/PaginationDataHolder.swift b/SideStore/Utils/pagination/PaginationDataHolder.swift
new file mode 100644
index 00000000..7ca804c5
--- /dev/null
+++ b/SideStore/Utils/pagination/PaginationDataHolder.swift
@@ -0,0 +1,93 @@
+//
+// PaginationDataHolder.swift
+// AltStore
+//
+// Created by Magesh K on 09/01/25.
+// Copyright © 2025 SideStore. All rights reserved.
+//
+
+import Foundation
+
+public class PaginationDataHolder {
+
+ public let itemsPerPage: UInt
+ public private(set) var currentPageindex: UInt
+
+ init(itemsPerPage: UInt, startPageIndex: UInt = 0) {
+ self.itemsPerPage = itemsPerPage
+ self.currentPageindex = startPageIndex
+ }
+
+ init(other: PaginationDataHolder) {
+ self.itemsPerPage = other.itemsPerPage
+ self.currentPageindex = other.currentPageindex
+ }
+
+ public enum PageLimitResult{
+ case null
+ case empty
+ case current
+ }
+
+ private func updatePageIndexForDirection(_ direction: Direction, itemsCount: Int) -> Bool {
+
+ var targetPageIndex = Int(currentPageindex)
+ let availablePages = UInt(ceil(Double(itemsCount) / Double(itemsPerPage)))
+
+ switch(direction){
+ case .up:
+ targetPageIndex -= 1
+ case .down:
+ targetPageIndex += 1
+ }
+
+ let isUpdateValid = (targetPageIndex >= 0 && targetPageIndex < availablePages)
+
+ if isUpdateValid{
+ self.currentPageindex = UInt(targetPageIndex)
+ }
+
+ return isUpdateValid
+ }
+
+ public func nextPage(inItems: [T], whenUnavailable: PageLimitResult = .current) -> [T]? {
+ return targetPage(for: .down, inItems: inItems, whenUnavailable: whenUnavailable)
+ }
+
+ public func prevPage(inItems: [T], whenUnavailable: PageLimitResult = .current) -> [T]? {
+ return targetPage(for: .up, inItems: inItems, whenUnavailable: whenUnavailable)
+ }
+
+ public func targetPage(for direction: Direction, inItems: [T], whenUnavailable: PageLimitResult = .current) -> [T]? {
+ if updatePageIndexForDirection(direction, itemsCount: inItems.count){
+ return currentPage(inItems: inItems)
+ }
+
+ switch whenUnavailable {
+ case .null:
+ return nil // null was requested
+ case .empty:
+ return [] // empty list was requested
+ case .current:
+ return currentPage(inItems: inItems) // Stay on the current page and return the same items
+ }
+ }
+
+ public func currentPage(inItems items: [T]) -> [T] {
+ let count = UInt(items.count)
+
+ if(count == 0) { return items }
+
+ // since we operate on any input items list at any time,
+ // we set currentPageIndex as last availablePage if out of bounds
+ let availablePages = UInt(ceil(Double(count) / Double(itemsPerPage)))
+
+ self.currentPageindex = min(availablePages-1, currentPageindex)
+
+ let startIndex = currentPageindex * itemsPerPage
+ let estimatedEndIndex = startIndex + (itemsPerPage-1)
+ let endIndex: UInt = min(count-1, estimatedEndIndex)
+ let currentPageEntries = items[Int(startIndex) ... Int(endIndex)]
+ return Array(currentPageEntries)
+ }
+}
diff --git a/SideStore/apps-v2.json b/SideStore/apps-v2.json
index 9166c28c..e0914d34 160000
--- a/SideStore/apps-v2.json
+++ b/SideStore/apps-v2.json
@@ -1 +1 @@
-Subproject commit 9166c28c455326d31cafe89acf2f7c9caa9384c1
+Subproject commit e0914d346315338d1600acda48d18982bb902fef
diff --git a/SideStore/minimuxer b/SideStore/minimuxer
index a23abf6d..baa22acb 160000
--- a/SideStore/minimuxer
+++ b/SideStore/minimuxer
@@ -1 +1 @@
-Subproject commit a23abf6d1cd2ed3e2d1dc6411e46558e0b5a26d1
+Subproject commit baa22acbff1dc00d76e4186e55ae1904eec428cc
diff --git a/update_apps.py b/update_apps.py
index 931fbd98..791e0077 100755
--- a/update_apps.py
+++ b/update_apps.py
@@ -4,16 +4,28 @@ import os
import json
import sys
+SIDESTORE_BUNDLE_ID = "com.SideStore.SideStore"
+
# Set environment variables with default values
-VERSION_IPA = os.getenv("VERSION_IPA", "0.0.0")
-VERSION_DATE = os.getenv("VERSION_DATE", "2000-12-18T00:00:00Z")
-BETA = os.getenv("BETA", "true").lower() == "true" # Convert to boolean
-COMMIT_ID = os.getenv("COMMIT_ID", "1234567")
-SIZE = int(os.getenv("SIZE", "0")) # Convert to integer
-SHA256 = os.getenv("SHA256", "")
-LOCALIZED_DESCRIPTION = os.getenv("LOCALIZED_DESCRIPTION", "Invalid Update")
-DOWNLOAD_URL = os.getenv("DOWNLOAD_URL", "https://github.com/SideStore/SideStore/releases/download/0.0.0/SideStore.ipa")
-BUNDLE_IDENTIFIER = os.getenv("BUNDLE_IDENTIFIER", "com.SideStore.SideStore")
+VERSION_IPA = os.getenv("VERSION_IPA")
+VERSION_DATE = os.getenv("VERSION_DATE")
+RELEASE_CHANNEL = os.getenv("RELEASE_CHANNEL")
+COMMIT_ID = os.getenv("COMMIT_ID")
+SIZE = os.getenv("SIZE")
+SHA256 = os.getenv("SHA256")
+LOCALIZED_DESCRIPTION = os.getenv("LOCALIZED_DESCRIPTION")
+DOWNLOAD_URL = os.getenv("DOWNLOAD_URL")
+BUNDLE_IDENTIFIER = os.getenv("BUNDLE_IDENTIFIER", SIDESTORE_BUNDLE_ID)
+
+# Uncomment to debug/test by simulating dummy input locally
+# VERSION_IPA = os.getenv("VERSION_IPA", "0.0.0")
+# VERSION_DATE = os.getenv("VERSION_DATE", "2000-12-18T00:00:00Z")
+# RELEASE_CHANNEL = os.getenv("RELEASE_CHANNEL", "alpha")
+# COMMIT_ID = os.getenv("COMMIT_ID", "1234567")
+# SIZE = int(os.getenv("SIZE", "0")) # Convert to integer
+# SHA256 = os.getenv("SHA256", "")
+# LOCALIZED_DESCRIPTION = os.getenv("LOCALIZED_DESCRIPTION", "Invalid Update")
+# DOWNLOAD_URL = os.getenv("DOWNLOAD_URL", "https://github.com/SideStore/SideStore/releases/download/0.0.0/SideStore.ipa")
# Check if input file is provided
if len(sys.argv) < 2:
@@ -24,9 +36,10 @@ input_file = sys.argv[1]
print(f"Input File: {input_file}")
# Debugging the environment variables
+print(" ====> Required parameter list <====")
print("Version:", VERSION_IPA)
print("Version Date:", VERSION_DATE)
-print("Beta:", BETA)
+print("ReleaseChannel:", RELEASE_CHANNEL)
print("Commit ID:", COMMIT_ID)
print("Size:", SIZE)
print("Sha256:", SHA256)
@@ -41,50 +54,70 @@ except Exception as e:
print(f"Error reading the input file: {e}")
sys.exit(1)
+if (VERSION_IPA == None or \
+ VERSION_DATE == None or \
+ RELEASE_CHANNEL == None or \
+ SIZE == None or \
+ SHA256 == None or \
+ LOCALIZED_DESCRIPTION == None or \
+ DOWNLOAD_URL == None):
+ print("One or more required parameter(s) were not defined as environment variable(s)")
+ sys.exit(1)
+
+# make it lowecase
+RELEASE_CHANNEL = RELEASE_CHANNEL.lower()
+# Convert to integer
+SIZE = int(SIZE)
+
+if RELEASE_CHANNEL != 'stable' and COMMIT_ID is None:
+ print("Commit ID cannot be empty when ReleaseChannel is not 'stable' ")
+ sys.exit(1)
+
+version = data.get("version")
+if int(version) < 2:
+ print("Only v2 and above are supported for direct updates to sources.json on push")
+ sys.exit(1)
+
# Process the JSON data
updated = False
for app in data.get("apps", []):
if app.get("bundleIdentifier") == BUNDLE_IDENTIFIER:
- # Update app-level metadata
- app.update({
- "version": VERSION_IPA,
- "versionDate": VERSION_DATE,
- "beta": BETA,
- "commitID": COMMIT_ID,
- "size": SIZE,
- "sha256": SHA256,
- "localizedDescription": LOCALIZED_DESCRIPTION,
- "downloadURL": DOWNLOAD_URL,
- })
+ if RELEASE_CHANNEL == "stable" :
+ # Update app-level metadata for store front page
+ app.update({
+ "version": VERSION_IPA,
+ "versionDate": VERSION_DATE,
+ "size": SIZE,
+ "sha256": SHA256,
+ "localizedDescription": LOCALIZED_DESCRIPTION,
+ "downloadURL": DOWNLOAD_URL,
+ })
# Process the versions array
- versions = app.get("versions", [])
- if not versions or not (versions[0].get("version") == VERSION_IPA and versions[0].get("beta") == BETA):
- # Prepend a new version if no matching version exists
- new_version = {
- "version": VERSION_IPA,
- "date": VERSION_DATE,
- "localizedDescription": LOCALIZED_DESCRIPTION,
- "downloadURL": DOWNLOAD_URL,
- "beta": BETA,
- "commitID": COMMIT_ID,
- "size": SIZE,
- "sha256": SHA256,
- }
- versions.insert(0, new_version)
+ channels = app.get("releaseChannels", {})
+ if not channels:
+ app["releaseChannels"] = channels
+
+ # create an entry and keep ready
+ new_version = {
+ "version": VERSION_IPA,
+ "date": VERSION_DATE,
+ "localizedDescription": LOCALIZED_DESCRIPTION,
+ "downloadURL": DOWNLOAD_URL,
+ "size": SIZE,
+ "sha256": SHA256,
+ }
+ # add commit ID if release is not stable
+ if RELEASE_CHANNEL != 'stable':
+ new_version["commitID"] = COMMIT_ID
+
+ if not channels.get(RELEASE_CHANNEL):
+ # there was no entries in this release channel so create one
+ channels[RELEASE_CHANNEL] = [new_version]
else:
- # Update the existing version object
- versions[0].update({
- "version": VERSION_IPA,
- "date": VERSION_DATE,
- "localizedDescription": LOCALIZED_DESCRIPTION,
- "downloadURL": DOWNLOAD_URL,
- "beta": BETA,
- "commitID": COMMIT_ID,
- "size": SIZE,
- "sha256": SHA256,
- })
- app["versions"] = versions
+ # Update the existing TOP version object entry
+ channels[RELEASE_CHANNEL][0] = new_version
+
updated = True
break
diff --git a/xcconfigs/AltBackup.xcconfig b/xcconfigs/AltBackup.xcconfig
index a283cc8a..fcb72751 100644
--- a/xcconfigs/AltBackup.xcconfig
+++ b/xcconfigs/AltBackup.xcconfig
@@ -1,6 +1,3 @@
#include "../Build.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltBackup
-
-// retain the non-suffixed bundleID set in $GROUP_ID for APP_GROUP_IDENTIFIER
-APP_GROUP_IDENTIFIER = $(GROUP_ID)
\ No newline at end of file
diff --git a/xcconfigs/AltWidgetExtension.xcconfig b/xcconfigs/AltWidgetExtension.xcconfig
index 2fd5d472..f269c9a7 100644
--- a/xcconfigs/AltWidgetExtension.xcconfig
+++ b/xcconfigs/AltWidgetExtension.xcconfig
@@ -1,6 +1,3 @@
#include "../Build.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).AltWidget
-
-// retain the non-suffixed bundleID set in $GROUP_ID for APP_GROUP_IDENTIFIER
-APP_GROUP_IDENTIFIER = $(GROUP_ID)
\ No newline at end of file