From dcf6fd5d51a79453001da1e0571a0db52f0002af Mon Sep 17 00:00:00 2001 From: Yaswanth Date: Thu, 25 Sep 2025 14:01:00 +0530 Subject: [PATCH 1/2] fix: update bundles in background using airborne --- .../ios/AirborneExample/AppDelegate.swift | 97 ++++- airborne-react-native/example/ios/Podfile | 2 + .../example/ios/Podfile.lock | 388 +++++++++--------- .../juspay/airborne/ota/ApplicationManager.kt | 279 ++++++++++++- .../java/in/juspay/airborne/ota/Constants.kt | 7 + .../java/in/juspay/airborne/ota/UpdateTask.kt | 81 +++- .../services/FileProviderService.java | 20 +- .../in/juspay/airborne/services/TempWriter.kt | 2 +- .../java/in/juspay/airborne/utils/OTAUtils.kt | 21 +- .../ApplicationManager/AJPResource.m | 2 +- .../Airborne/AirborneSwift/Airborne.swift | 15 +- 11 files changed, 654 insertions(+), 260 deletions(-) diff --git a/airborne-react-native/example/ios/AirborneExample/AppDelegate.swift b/airborne-react-native/example/ios/AirborneExample/AppDelegate.swift index 375f7825..7e62692f 100644 --- a/airborne-react-native/example/ios/AirborneExample/AppDelegate.swift +++ b/airborne-react-native/example/ios/AirborneExample/AppDelegate.swift @@ -27,26 +27,62 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Create the main window early self.window = UIWindow(frame: UIScreen.main.bounds) +// let delegate = ReactNativeDelegate(customPath: bundlePath) +// let factory = RCTReactNativeFactory(delegate: delegate) +// delegate.dependencyProvider = RCTAppDependencyProvider() +// +// reactNativeDelegate = delegate +// reactNativeFactory = factory +// +// factory.startReactNative( +// withModuleName: "AirborneExample", +// in: window, +// launchOptions: self.launchOptions +// ) + + +// let delegate = ReactNativeDelegate() +// let factory = RCTReactNativeFactory(delegate: delegate) +// delegate.dependencyProvider = RCTAppDependencyProvider() +// +// reactNativeDelegate = delegate +// reactNativeFactory = factory +// +// window = UIWindow(frame: UIScreen.main.bounds) +// +// factory.startReactNative( +// withModuleName: "AirborneExample", +// in: window, +// launchOptions: launchOptions +// ) + return true } private func initializeHyperOTA() { - airborne = Airborne(releaseConfigURL: "https://airborne.sandbox.juspay.in/release/airborne-react-example/ios", delegate: self) + airborne = Airborne(releaseConfigURL: "http://127.0.0.1:8080/release_config.json", delegate: self) print("HyperOTA: Initialized successfully") } } extension AppDelegate: AirborneDelegate { - func getNamespace() -> String { + func namespace() -> String { return "airborneexample" } - func getBundle() -> Bundle { - return Bundle.main + func bundle() -> Bundle { + guard + let bundleURL = Bundle.main.url(forResource: "airborneex", withExtension: "bundle"), + let bundle = Bundle(url: bundleURL) + else { + fatalError("❌ Could not find airborneex.bundle in main bundle.") + } + + return bundle } - func getDimensions() -> [String : String] { + func dimensions() -> [String : String] { return [:] } @@ -54,32 +90,37 @@ extension AppDelegate: AirborneDelegate { print("Event: \(key) = \(value)") } - func startApp(_ bundlePath: String) { + + func startApp(indexBundleURL: URL) -> Void { DispatchQueue.main.async { [self] in - let delegate = ReactNativeDelegate(customPath: bundlePath) + let bundlePath = indexBundleURL.absoluteString + + print("In start APP \(bundlePath)") + let delegate = ReactNativeDelegate(bundleURL: indexBundleURL) let factory = RCTReactNativeFactory(delegate: delegate) delegate.dependencyProvider = RCTAppDependencyProvider() - + reactNativeDelegate = delegate reactNativeFactory = factory - + factory.startReactNative( withModuleName: "AirborneExample", in: window, launchOptions: self.launchOptions ) - + } } + } class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { - private let customPath: String + private let bundleUrl: URL - init(customPath: String) { - self.customPath = customPath + init(bundleURL: URL) { + self.bundleUrl = bundleURL super.init() } @@ -88,10 +129,30 @@ class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { } override func bundleURL() -> URL? { -#if DEBUG - RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") -#else - URL(fileURLWithPath: customPath) -#endif + +// if let bundleURL = Bundle.main.url(forResource: "airborneex", withExtension: "bundle") { +// return bundleURL.appendingPathComponent("main.jsbundle") +// } + + return self.bundleUrl } } + +//class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { +// override func sourceURL(for bridge: RCTBridge) -> URL? { +// self.bundleURL() +// } +// +// override func bundleURL() -> URL? { +// +// if let bundleURL = Bundle.main.url(forResource: "airborneex", withExtension: "bundle") { +// return bundleURL.appendingPathComponent("main.jsbundle") +// } +// +//#if DEBUG +// RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") +//#else +// return Bundle.main.url(forResource: "main", withExtension: "jsbundle") +//#endif +// } +//} diff --git a/airborne-react-native/example/ios/Podfile b/airborne-react-native/example/ios/Podfile index f7c858e9..e299d843 100644 --- a/airborne-react-native/example/ios/Podfile +++ b/airborne-react-native/example/ios/Podfile @@ -25,6 +25,8 @@ target 'AirborneExample' do # An absolute path to your application root. :app_path => "#{Pod::Config.instance.installation_root}/.." ) + + pod 'FLEX', :configurations => ['Debug'] post_install do |installer| # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 diff --git a/airborne-react-native/example/ios/Podfile.lock b/airborne-react-native/example/ios/Podfile.lock index 37f3d041..2b73be41 100644 --- a/airborne-react-native/example/ios/Podfile.lock +++ b/airborne-react-native/example/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - Airborne (0.3.2) - - AirborneReact (0.3.0): + - AirborneReact (0.13.4): - Airborne (= 0.3.2) - DoubleConversion - glog @@ -29,6 +29,7 @@ PODS: - DoubleConversion (1.1.6) - fast_float (6.1.4) - FBLazyVector (0.79.2) + - FLEX (5.22.10) - fmt (11.0.2) - glog (0.3.5) - hermes-engine (0.79.2): @@ -1685,304 +1686,307 @@ PODS: DEPENDENCIES: - AirborneReact (from `../..`) - - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) - - RCTRequired (from `../node_modules/react-native/Libraries/Required`) - - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) - - React (from `../node_modules/react-native/`) - - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) - - React-Core (from `../node_modules/react-native/`) - - React-Core/RCTWebSocket (from `../node_modules/react-native/`) - - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) - - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) - - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) - - React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) - - React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`) - - React-Fabric (from `../node_modules/react-native/ReactCommon`) - - React-FabricComponents (from `../node_modules/react-native/ReactCommon`) - - React-FabricImage (from `../node_modules/react-native/ReactCommon`) - - React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`) - - React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) - - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`) - - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) - - React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) - - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) - - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`) - - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) - - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) - - React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) - - React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`) - - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) - - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`) - - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - - React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`) - - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) - - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) - - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) - - React-RCTFabric (from `../node_modules/react-native/React`) - - React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`) - - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) - - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) - - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) - - React-RCTRuntime (from `../node_modules/react-native/React/Runtime`) - - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) - - React-RCTText (from `../node_modules/react-native/Libraries/Text`) - - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) - - React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`) - - React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`) - - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) - - React-rncore (from `../node_modules/react-native/ReactCommon`) - - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) - - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`) - - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) - - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`) - - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) - - React-timing (from `../node_modules/react-native/ReactCommon/react/timing`) - - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) + - boost (from `../../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DoubleConversion (from `../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - fast_float (from `../../node_modules/react-native/third-party-podspecs/fast_float.podspec`) + - FBLazyVector (from `../../node_modules/react-native/Libraries/FBLazyVector`) + - FLEX + - fmt (from `../../node_modules/react-native/third-party-podspecs/fmt.podspec`) + - glog (from `../../node_modules/react-native/third-party-podspecs/glog.podspec`) + - hermes-engine (from `../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - RCT-Folly (from `../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCT-Folly/Fabric (from `../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCTDeprecation (from `../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) + - RCTRequired (from `../../node_modules/react-native/Libraries/Required`) + - RCTTypeSafety (from `../../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../../node_modules/react-native/`) + - React-callinvoker (from `../../node_modules/react-native/ReactCommon/callinvoker`) + - React-Core (from `../../node_modules/react-native/`) + - React-Core/RCTWebSocket (from `../../node_modules/react-native/`) + - React-CoreModules (from `../../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../../node_modules/react-native/ReactCommon/cxxreact`) + - React-debug (from `../../node_modules/react-native/ReactCommon/react/debug`) + - React-defaultsnativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) + - React-domnativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/dom`) + - React-Fabric (from `../../node_modules/react-native/ReactCommon`) + - React-FabricComponents (from `../../node_modules/react-native/ReactCommon`) + - React-FabricImage (from `../../node_modules/react-native/ReactCommon`) + - React-featureflags (from `../../node_modules/react-native/ReactCommon/react/featureflags`) + - React-featureflagsnativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) + - React-graphics (from `../../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../../node_modules/react-native/ReactCommon/hermes`) + - React-idlecallbacksnativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) + - React-ImageManager (from `../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) + - React-jserrorhandler (from `../../node_modules/react-native/ReactCommon/jserrorhandler`) + - React-jsi (from `../../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsinspectortracing (from `../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) + - React-jsitooling (from `../../node_modules/react-native/ReactCommon/jsitooling`) + - React-jsitracing (from `../../node_modules/react-native/ReactCommon/hermes/executor/`) + - React-logger (from `../../node_modules/react-native/ReactCommon/logger`) + - React-Mapbuffer (from `../../node_modules/react-native/ReactCommon`) + - React-microtasksnativemodule (from `../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - React-NativeModulesApple (from `../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-oscompat (from `../../node_modules/react-native/ReactCommon/oscompat`) + - React-perflogger (from `../../node_modules/react-native/ReactCommon/reactperflogger`) + - React-performancetimeline (from `../../node_modules/react-native/ReactCommon/react/performance/timeline`) + - React-RCTActionSheet (from `../../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../../node_modules/react-native/Libraries/Blob`) + - React-RCTFabric (from `../../node_modules/react-native/React`) + - React-RCTFBReactNativeSpec (from `../../node_modules/react-native/React`) + - React-RCTImage (from `../../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../../node_modules/react-native/Libraries/Network`) + - React-RCTRuntime (from `../../node_modules/react-native/React/Runtime`) + - React-RCTSettings (from `../../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../../node_modules/react-native/Libraries/Vibration`) + - React-rendererconsistency (from `../../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-renderercss (from `../../node_modules/react-native/ReactCommon/react/renderer/css`) + - React-rendererdebug (from `../../node_modules/react-native/ReactCommon/react/renderer/debug`) + - React-rncore (from `../../node_modules/react-native/ReactCommon`) + - React-RuntimeApple (from `../../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) + - React-RuntimeCore (from `../../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimeexecutor (from `../../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-RuntimeHermes (from `../../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimescheduler (from `../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-timing (from `../../node_modules/react-native/ReactCommon/react/timing`) + - React-utils (from `../../node_modules/react-native/ReactCommon/react/utils`) - ReactAppDependencyProvider (from `build/generated/ios`) - ReactCodegen (from `build/generated/ios`) - - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) + - ReactCommon/turbomodule/core (from `../../node_modules/react-native/ReactCommon`) + - Yoga (from `../../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: trunk: - Airborne + - FLEX - SocketRocket EXTERNAL SOURCES: AirborneReact: :path: "../.." boost: - :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + :podspec: "../../node_modules/react-native/third-party-podspecs/boost.podspec" DoubleConversion: - :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + :podspec: "../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: - :podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec" + :podspec: "../../node_modules/react-native/third-party-podspecs/fast_float.podspec" FBLazyVector: - :path: "../node_modules/react-native/Libraries/FBLazyVector" + :path: "../../node_modules/react-native/Libraries/FBLazyVector" fmt: - :podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec" + :podspec: "../../node_modules/react-native/third-party-podspecs/fmt.podspec" glog: - :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + :podspec: "../../node_modules/react-native/third-party-podspecs/glog.podspec" hermes-engine: - :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :podspec: "../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" :tag: hermes-2025-03-03-RNv0.79.0-bc17d964d03743424823d7dd1a9f37633459c5c5 RCT-Folly: - :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" + :podspec: "../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTDeprecation: - :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" + :path: "../../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" RCTRequired: - :path: "../node_modules/react-native/Libraries/Required" + :path: "../../node_modules/react-native/Libraries/Required" RCTTypeSafety: - :path: "../node_modules/react-native/Libraries/TypeSafety" + :path: "../../node_modules/react-native/Libraries/TypeSafety" React: - :path: "../node_modules/react-native/" + :path: "../../node_modules/react-native/" React-callinvoker: - :path: "../node_modules/react-native/ReactCommon/callinvoker" + :path: "../../node_modules/react-native/ReactCommon/callinvoker" React-Core: - :path: "../node_modules/react-native/" + :path: "../../node_modules/react-native/" React-CoreModules: - :path: "../node_modules/react-native/React/CoreModules" + :path: "../../node_modules/react-native/React/CoreModules" React-cxxreact: - :path: "../node_modules/react-native/ReactCommon/cxxreact" + :path: "../../node_modules/react-native/ReactCommon/cxxreact" React-debug: - :path: "../node_modules/react-native/ReactCommon/react/debug" + :path: "../../node_modules/react-native/ReactCommon/react/debug" React-defaultsnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults" + :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/defaults" React-domnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom" + :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/dom" React-Fabric: - :path: "../node_modules/react-native/ReactCommon" + :path: "../../node_modules/react-native/ReactCommon" React-FabricComponents: - :path: "../node_modules/react-native/ReactCommon" + :path: "../../node_modules/react-native/ReactCommon" React-FabricImage: - :path: "../node_modules/react-native/ReactCommon" + :path: "../../node_modules/react-native/ReactCommon" React-featureflags: - :path: "../node_modules/react-native/ReactCommon/react/featureflags" + :path: "../../node_modules/react-native/ReactCommon/react/featureflags" React-featureflagsnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" + :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" React-graphics: - :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics" + :path: "../../node_modules/react-native/ReactCommon/react/renderer/graphics" React-hermes: - :path: "../node_modules/react-native/ReactCommon/hermes" + :path: "../../node_modules/react-native/ReactCommon/hermes" React-idlecallbacksnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" + :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" React-ImageManager: - :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" + :path: "../../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" React-jserrorhandler: - :path: "../node_modules/react-native/ReactCommon/jserrorhandler" + :path: "../../node_modules/react-native/ReactCommon/jserrorhandler" React-jsi: - :path: "../node_modules/react-native/ReactCommon/jsi" + :path: "../../node_modules/react-native/ReactCommon/jsi" React-jsiexecutor: - :path: "../node_modules/react-native/ReactCommon/jsiexecutor" + :path: "../../node_modules/react-native/ReactCommon/jsiexecutor" React-jsinspector: - :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" + :path: "../../node_modules/react-native/ReactCommon/jsinspector-modern" React-jsinspectortracing: - :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" + :path: "../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" React-jsitooling: - :path: "../node_modules/react-native/ReactCommon/jsitooling" + :path: "../../node_modules/react-native/ReactCommon/jsitooling" React-jsitracing: - :path: "../node_modules/react-native/ReactCommon/hermes/executor/" + :path: "../../node_modules/react-native/ReactCommon/hermes/executor/" React-logger: - :path: "../node_modules/react-native/ReactCommon/logger" + :path: "../../node_modules/react-native/ReactCommon/logger" React-Mapbuffer: - :path: "../node_modules/react-native/ReactCommon" + :path: "../../node_modules/react-native/ReactCommon" React-microtasksnativemodule: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" React-NativeModulesApple: - :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + :path: "../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" React-oscompat: - :path: "../node_modules/react-native/ReactCommon/oscompat" + :path: "../../node_modules/react-native/ReactCommon/oscompat" React-perflogger: - :path: "../node_modules/react-native/ReactCommon/reactperflogger" + :path: "../../node_modules/react-native/ReactCommon/reactperflogger" React-performancetimeline: - :path: "../node_modules/react-native/ReactCommon/react/performance/timeline" + :path: "../../node_modules/react-native/ReactCommon/react/performance/timeline" React-RCTActionSheet: - :path: "../node_modules/react-native/Libraries/ActionSheetIOS" + :path: "../../node_modules/react-native/Libraries/ActionSheetIOS" React-RCTAnimation: - :path: "../node_modules/react-native/Libraries/NativeAnimation" + :path: "../../node_modules/react-native/Libraries/NativeAnimation" React-RCTAppDelegate: - :path: "../node_modules/react-native/Libraries/AppDelegate" + :path: "../../node_modules/react-native/Libraries/AppDelegate" React-RCTBlob: - :path: "../node_modules/react-native/Libraries/Blob" + :path: "../../node_modules/react-native/Libraries/Blob" React-RCTFabric: - :path: "../node_modules/react-native/React" + :path: "../../node_modules/react-native/React" React-RCTFBReactNativeSpec: - :path: "../node_modules/react-native/React" + :path: "../../node_modules/react-native/React" React-RCTImage: - :path: "../node_modules/react-native/Libraries/Image" + :path: "../../node_modules/react-native/Libraries/Image" React-RCTLinking: - :path: "../node_modules/react-native/Libraries/LinkingIOS" + :path: "../../node_modules/react-native/Libraries/LinkingIOS" React-RCTNetwork: - :path: "../node_modules/react-native/Libraries/Network" + :path: "../../node_modules/react-native/Libraries/Network" React-RCTRuntime: - :path: "../node_modules/react-native/React/Runtime" + :path: "../../node_modules/react-native/React/Runtime" React-RCTSettings: - :path: "../node_modules/react-native/Libraries/Settings" + :path: "../../node_modules/react-native/Libraries/Settings" React-RCTText: - :path: "../node_modules/react-native/Libraries/Text" + :path: "../../node_modules/react-native/Libraries/Text" React-RCTVibration: - :path: "../node_modules/react-native/Libraries/Vibration" + :path: "../../node_modules/react-native/Libraries/Vibration" React-rendererconsistency: - :path: "../node_modules/react-native/ReactCommon/react/renderer/consistency" + :path: "../../node_modules/react-native/ReactCommon/react/renderer/consistency" React-renderercss: - :path: "../node_modules/react-native/ReactCommon/react/renderer/css" + :path: "../../node_modules/react-native/ReactCommon/react/renderer/css" React-rendererdebug: - :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" + :path: "../../node_modules/react-native/ReactCommon/react/renderer/debug" React-rncore: - :path: "../node_modules/react-native/ReactCommon" + :path: "../../node_modules/react-native/ReactCommon" React-RuntimeApple: - :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios" + :path: "../../node_modules/react-native/ReactCommon/react/runtime/platform/ios" React-RuntimeCore: - :path: "../node_modules/react-native/ReactCommon/react/runtime" + :path: "../../node_modules/react-native/ReactCommon/react/runtime" React-runtimeexecutor: - :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" + :path: "../../node_modules/react-native/ReactCommon/runtimeexecutor" React-RuntimeHermes: - :path: "../node_modules/react-native/ReactCommon/react/runtime" + :path: "../../node_modules/react-native/ReactCommon/react/runtime" React-runtimescheduler: - :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + :path: "../../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" React-timing: - :path: "../node_modules/react-native/ReactCommon/react/timing" + :path: "../../node_modules/react-native/ReactCommon/react/timing" React-utils: - :path: "../node_modules/react-native/ReactCommon/react/utils" + :path: "../../node_modules/react-native/ReactCommon/react/utils" ReactAppDependencyProvider: :path: build/generated/ios ReactCodegen: :path: build/generated/ios ReactCommon: - :path: "../node_modules/react-native/ReactCommon" + :path: "../../node_modules/react-native/ReactCommon" Yoga: - :path: "../node_modules/react-native/ReactCommon/yoga" + :path: "../../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: Airborne: 46578e56070672d064940d803c149007d747a6f8 - AirborneReact: cf25bdffe669629d0fe2b1b5cb3df68b318409d0 + AirborneReact: b461f873ab698eaf4dc3cb3407f228ed6e655874 boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: 84b955f7b4da8b895faf5946f73748267347c975 + FLEX: f21ee4f498eed3f8a1eded66b21939fd3b7a22ce fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 314be5250afa5692b57b4dd1705959e1973a8ebe - RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82 + RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809 RCTDeprecation: 83ffb90c23ee5cea353bd32008a7bca100908f8c RCTRequired: eb7c0aba998009f47a540bec9e9d69a54f68136e RCTTypeSafety: 659ae318c09de0477fd27bbc9e140071c7ea5c93 React: c2d3aa44c49bb34e4dfd49d3ee92da5ebacc1c1c React-callinvoker: 1bdfb7549b5af266d85757193b5069f60659ef9d - React-Core: 10597593fdbae06f0089881e025a172e51d4a769 - React-CoreModules: 6907b255529dd46895cf687daa67b24484a612c2 - React-cxxreact: a9f5b8180d6955bc3f6a3fcd657c4d9b4d95c1f6 + React-Core: 7150cf9b6a5af063b37003062689f1691e79c020 + React-CoreModules: 15a85e6665d61678942da6ae485b351f4c699049 + React-cxxreact: 74f9de59259ac951923f5726aa14f0398f167af9 React-debug: a9861ea2196e886642887e29fd1d86c6eee93454 - React-defaultsnativemodule: 48bed05d5e7a6b90c63775bc042acba50f1ac46c - React-domnativemodule: 4603dc552b8f2b75cfd708b6175f0f3ab005d661 - React-Fabric: c48e870a557e39fc38c49bf2f43a62f837876318 - React-FabricComponents: d0c0029b9066819e736ee70ce8481fe52632ccad - React-FabricImage: a843d50c9d21f4a6255c3cd1654cdf16209ac76d + React-defaultsnativemodule: 0e85419763d183f937ec98ea01a27a2afd6f1aa2 + React-domnativemodule: 9b970dcaf8fe53250622daba1d42877f400b858b + React-Fabric: 0be44adc205b650b4f6003fd8dd3d72b3a9ba6a8 + React-FabricComponents: ea062466367976df752a9d5847c7136a7f07bb3d + React-FabricImage: 3add4d33413771218a062920378f3dac2abe6502 React-featureflags: 4ef61c283dfae8f327dbae70f41bb0399bd9e0fc - React-featureflagsnativemodule: 879cdf94179dc7395f28b9bf0fd340fd45c05ab7 - React-graphics: 4e247c50991de6e2c0abd25f8367cfa3113198c0 - React-hermes: 9116d4e6d07abeb519a2852672de087f44da8f12 - React-idlecallbacksnativemodule: 5fd6d838b045d3f5b630a6a0714635bc9c882fdc - React-ImageManager: ad3f561d76883d6f7f1cf3a97e823fa39ca1b132 - React-jserrorhandler: 35d127a39a5bc16d9ae97edce7ab4c06dc77e3a2 - React-jsi: 753ba30c902f3a41fa7f956aca8eea3317a44ee6 - React-jsiexecutor: 47520714aa7d9589c51c0f3713dfbfca4895d4f9 - React-jsinspector: ec984e95482ee98692ec74f78771447599ed9781 - React-jsinspectortracing: 7bd661f34f08b320bb797dc464d6002116fca145 - React-jsitooling: 90d7ecbb60f70d12f60a4964dfcad0e38d9d970d - React-jsitracing: a9de0d25bf430574dc01f1fe67f06fd50e8a578c - React-logger: 8edfcedc100544791cd82692ca5a574240a16219 - React-Mapbuffer: da73f30b000114058d6bc41490dcce204a8ede32 - React-microtasksnativemodule: 444c5701aece79629bb73bd9e7ad8937ae65238c - React-NativeModulesApple: df8e5bc59e78ca3040ffbf41336889f3bd0fad68 + React-featureflagsnativemodule: 3324bd2b55b374897914cf8c6d03cc3c35889d86 + React-graphics: 29570df7b0b3d210ba5a4ec67024b117b5a8c74a + React-hermes: 8b86e5f54a65ecb69cdf22b3a00a11562eda82d2 + React-idlecallbacksnativemodule: 8624d07a1b25045419521d5809cdf3c948dbc411 + React-ImageManager: 21a10bb92dde7cf5ec29c139769630dadbac0cc2 + React-jserrorhandler: 00697e19cde827e8134d543b404f3f016616e8bc + React-jsi: 6af1987cfbb1b6621664fdbf6c7b62bd4d38c923 + React-jsiexecutor: 51f372998e0303585cb0317232b938d694663cbd + React-jsinspector: d797824c7b7c931c5ad8ab41de3841a3d7a52e6f + React-jsinspectortracing: d9acc748525e22e4c7b7241ea39ecbd99bfa6c22 + React-jsitooling: 5bf7d347f145ae47bcecd4107ba7fe7539ebf55b + React-jsitracing: 6e27cce907f23131472594523cd6e30737a754e5 + React-logger: 368570a253f00879a1e4fea24ed4047e72e7bbf3 + React-Mapbuffer: f581f01e9c766bbfb7d1f57a6f6ceb86bdc310f1 + React-microtasksnativemodule: 8fc1bfccd27980ed6cfec1f52ffcd6af336f48f7 + React-NativeModulesApple: 1b5e6bf9164371771e553f744da801f5d9f5c7e5 React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c - React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d - React-performancetimeline: d6a5fd3640c873875badb33020ebe5af46ef2a12 + React-perflogger: 6fd2f6811533e9c19a61e855c3033eecbf4ad2a0 + React-performancetimeline: df331d0764cc54204a73960408371111efbd34b7 React-RCTActionSheet: a499b0d6d9793886b67ba3e16046a3fef2cdbbc3 - React-RCTAnimation: cc64adc259aabc3354b73065e2231d796dfce576 - React-RCTAppDelegate: 9d523da768f1c9e84c5f3b7e3624d097dfb0e16b - React-RCTBlob: e727f53eeefded7e6432eb76bd22b57bc880e5d1 - React-RCTFabric: a704a0eea3a9a32621ccc79261fe5b010bfcaccb - React-RCTFBReactNativeSpec: 9064c63d99e467a3893e328ba3612745c3c3a338 - React-RCTImage: 7159cbdbb18a09d97ba1a611416eced75b3ccb29 - React-RCTLinking: 46293afdb859bccc63e1d3dedc6901a3c04ef360 - React-RCTNetwork: 4a6cd18f5bcd0363657789c64043123a896b1170 - React-RCTRuntime: 4b47b6380420e9fbfb4bd3cc16d6ea988640ddc1 - React-RCTSettings: 61e361dc85136d1cb0e148b7541993d2ee950ea7 - React-RCTText: abd1e196c3167175e6baef18199c6d9d8ac54b4e - React-RCTVibration: 490e0dcb01a3fe4a0dfb7bc51ad5856d8b84f343 + React-RCTAnimation: 2595dcb10a82216a511b54742f8c28d793852ac6 + React-RCTAppDelegate: f03604b70f57c9469a84a159d8abecf793a5bcff + React-RCTBlob: e00f9b4e2f151938f4d9864cf33ebf24ac03328a + React-RCTFabric: d3c06acaa6f6b4f51b37b24c1ae5393f55a2ff22 + React-RCTFBReactNativeSpec: 0f4d4f0da938101f2ca9d5333a8f46e527ad2819 + React-RCTImage: dac5e9f8ec476aefe6e60ee640ebc1dfaf1a4dbe + React-RCTLinking: 494b785a40d952a1dfbe712f43214376e5f0e408 + React-RCTNetwork: b3d7c30cd21793e268db107dd0980cb61b3c1c44 + React-RCTRuntime: 8137d4927804480c952a40c422e40a6e697c616f + React-RCTSettings: a060c7e381a3896104761b8eed7e284d95e37df3 + React-RCTText: 4f272b72dbb61f390d8c8274528f9fdbff983806 + React-RCTVibration: 0e5326220719aca12473d703aa46693e3b4ce67a React-rendererconsistency: 68db5a64f0c42b0337e25ba7b0e9513caae1389d - React-renderercss: 59c892b54a92f2f62b98c2700f5ff7592f0629cb - React-rendererdebug: f9dfe8ef736c98268f87114d05f49615f7f9af46 + React-renderercss: eeb482d2790028a9b47ce9c214397ae2f4e2a7c5 + React-rendererdebug: c6e3b7583c2f0802cb3b4cf19f714853fa9ac670 React-rncore: 0f64cacb1becc6f89c99018ca920d012f9044ebd - React-RuntimeApple: f2bc2dc51b9b3c194c0eec4351ae99d033485792 - React-RuntimeCore: 4e5475d506e7f9c4b3f3c6e6a654b83cba7f1a42 + React-RuntimeApple: 3d34bd566375cbb61e94360329835fde05ced395 + React-RuntimeCore: 6140c26964655b66ddc8afb744c9df429f833357 React-runtimeexecutor: d60846710facedd1edb70c08b738119b3ee2c6c2 - React-RuntimeHermes: 2b238368a56fc50e9372afde7a3f2eeb0cdc63a7 - React-runtimescheduler: c65050ab5c3911dd553bb9223c76543198c5854f + React-RuntimeHermes: 00ee3577308f9644df029168aa874ac75f25d710 + React-runtimescheduler: 2ad0d683dd8eac942e03cea82d45982fd5fac362 React-timing: beb0ba912f9ffc1a6758afa767ae5c03302dc9ee - React-utils: 4b32801a05eff845d316edd59b313a3e25c5fc08 - ReactAppDependencyProvider: 04d5eb15eb46be6720e17a4a7fa92940a776e584 - ReactCodegen: 041559ba76d00f6680dfa0916b3c791f4babe5ea - ReactCommon: 1511ef100f1afa4c199fe52fe7a8d2529a41429a + React-utils: 5593ad221337dddfc69230fc32e94af714773ce5 + ReactAppDependencyProvider: d5dcc564f129632276bd3184e60f053fcd574d6b + ReactCodegen: 515cfe558eb7d990b29a84770726dd0233c8a54c + ReactCommon: 35a5629d7159876dd048c63751eaeb3aa3a6a53b SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: 50518ade05048235d91a78b803336dbb5b159d5d + Yoga: aa3de42163965879e7d68a5caaf3e1cedb2cbeb2 -PODFILE CHECKSUM: a276e8c93cf89c5b187cb3312148f548b87fe781 +PODFILE CHECKSUM: a6cd53a4f983768b6c94aa8e463fb62aeea0ad80 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/ApplicationManager.kt b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/ApplicationManager.kt index b5992373..6b0504ff 100644 --- a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/ApplicationManager.kt +++ b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/ApplicationManager.kt @@ -26,7 +26,6 @@ import `in`.juspay.airborne.ota.Constants.DEFAULT_RESOURCES import `in`.juspay.airborne.ota.Constants.PACKAGE_DIR_NAME import `in`.juspay.airborne.ota.Constants.PACKAGE_MANIFEST_FILE_NAME import `in`.juspay.airborne.ota.Constants.RC_VERSION_FILE_NAME -import `in`.juspay.airborne.ota.Constants.RESOURCES_DIR_NAME import `in`.juspay.airborne.ota.Constants.RESOURCES_FILE_NAME import `in`.juspay.airborne.services.OTAServices import `in`.juspay.airborne.utils.OTAUtils @@ -40,6 +39,11 @@ import java.util.concurrent.Callable import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap import java.util.concurrent.Future +import `in`.juspay.airborne.ota.Constants.APP_DIR +import `in`.juspay.airborne.ota.Constants.BACKUP_DIR +import `in`.juspay.airborne.ota.Constants.BACKUP_MAIN +import `in`.juspay.airborne.ota.Constants.BACKUP_TEMP +import org.json.JSONException class ApplicationManager( private val ctx: Context, @@ -61,6 +65,8 @@ class ApplicationManager( private var indexFolderPath = "" private var sessionId: String? = null private var rcCallback: ReleaseConfigCallback? = null + private val backupTempDir = "$BACKUP_DIR/$BACKUP_TEMP" + private val backupMainDir = "$BACKUP_DIR/$BACKUP_MAIN" fun loadApplication( unSanitizedClientId: String, @@ -83,6 +89,7 @@ class ApplicationManager( true } val contextRef = CONTEXT_MAP[clientId] ?: newRef + completeRollback(clientId) releaseConfig = readReleaseConfig(contextRef) if (shouldUpdate) { releaseConfig = @@ -130,7 +137,7 @@ class ApplicationManager( fun getIndexBundlePath(): String { while (!indexPathWaitTask.isDone) { try { - (ctx as java.lang.Object).wait() + (ctx as Object).wait() } catch (e: Exception) { // ignore } @@ -146,6 +153,13 @@ class ApplicationManager( return releaseConfig?.pkg?.index?.filePath ?: "" } + fun completeRollback(clientId: String) { + val rollbackInProgress = workspace.getFromSharedPreference(Constants.ROLLBACK_IN_PROGRESS, "false") + if (rollbackInProgress == "true") { + rollbackOTA(clientId) + } + } + private fun tryUpdate( clientId: String, initialized: Boolean, @@ -156,11 +170,13 @@ class ApplicationManager( val url = if (releaseConfigTemplateUrl == "") rcCallback?.getReleaseConfig(false) else releaseConfigTemplateUrl netUtils = OTANetUtils(ctx, clientId, otaServices.cleanUpValue) netUtils.setTrackMetrics(metricsEndPoint != null) + val blackListedVersions = getRolledBackVersions() val newTask = UpdateTask( url ?: releaseConfigTemplateUrl, otaServices.fileProviderService, releaseConfig, + blackListedVersions, fileLock, tracker, netUtils, @@ -217,6 +233,7 @@ class ApplicationManager( else -> releaseConfig } logTimeTaken(startTime, "tryUpdate") + checkIfBackupPending() //TODO: This can backup the next version also. Evaluate how that can be stopped. return rc } @@ -243,17 +260,9 @@ class ApplicationManager( releaseConfig?.resources?.filePaths ?: emptyList() val newResourceFiles = updatedRc?.resources?.filePaths ?: emptyList() - val splits = if (fromAirborne) { - pkgSplits + newPkgSplits + resourceFiles + newResourceFiles - } else { - (pkgSplits + newPkgSplits) - } + val splits = pkgSplits + newPkgSplits + resourceFiles + newResourceFiles cleanUpDir(pkgDir, splits) - if (!fromAirborne) { - cleanUpDir("app/$RESOURCES_DIR_NAME", resourceFiles + newResourceFiles) - } - val savedPkgDir = persistentState.optJSONObject(StateKey.SAVED_PACKAGE_UPDATE.name) ?.optString("dir") val savedResDir = persistentState.optJSONObject(StateKey.SAVED_RESOURCE_UPDATE.name) @@ -314,6 +323,16 @@ class ApplicationManager( logTimeTaken(startTime) } + private fun checkIfBackupPending() { + doAsync { + val stage = workspace.getFromSharedPreference(Constants.BACKUP_STAGE, "") + if (stage == "" || stage == BACKUP_STAGES.DONE.name || stage == BACKUP_STAGES.FAILED.name) { + return@doAsync + } + backupOTA() + } + } + fun readResourceByName(name: String): String { val filePath = releaseConfig?.resources?.getResource(name)?.filePath val text = filePath?.let { readResourceByFileName(it) } ?: "" @@ -342,7 +361,7 @@ class ApplicationManager( } private fun readResourceByFileName(filePath: String): String = - readFile("$RESOURCES_DIR_NAME/$filePath") + readFile("$PACKAGE_DIR_NAME/$filePath") private fun readReleaseConfig(lock: Any): ReleaseConfig? { // TODO big change, need to do server change @@ -375,6 +394,11 @@ class ApplicationManager( return null } + private fun readReleaseConfigFromDir(folderPath: String): List { + return listOf(RC_VERSION_FILE_NAME, CONFIG_FILE_NAME, PACKAGE_MANIFEST_FILE_NAME, RESOURCES_FILE_NAME) + .map { otaServices.fileProviderService.readFromInternalStorage("$folderPath/$it") } + } + private fun loadConfigComponent( content: String, fileName: String, @@ -499,13 +523,235 @@ class ApplicationManager( private fun getIndexFilePath(fileName: String): String { val file = - otaServices.fileProviderService.getFileFromInternalStorage("app/$PACKAGE_DIR_NAME/$fileName") + otaServices.fileProviderService.getFileFromInternalStorage("$APP_DIR/$PACKAGE_DIR_NAME/$fileName") if (file.exists()) { return file.absolutePath } return "" } + /** + * Creates a restore point which can be used in rollback. + */ + fun createRestorePoint() { + backupOTA() + } + + internal fun backupOTA(): Boolean { + val stage = workspace.getFromSharedPreference(Constants.BACKUP_STAGE, "") + + val (rcVersion, config, pkg, res) = readReleaseConfigFromDir("$backupMainDir/$APP_DIR") + val (curRcVersion, curConfig, curPkg, curRes) = readReleaseConfigFromDir(APP_DIR) + + if (stage == BACKUP_STAGES.DONE.name && rcVersion == curRcVersion && config == curConfig && pkg == curPkg && res == curRes) { + return true + } + + if (BACKUP_STAGES.COPY_TO_BACKUP_IN_PROGRESS.name == stage) { + // The backup is incomplete in this case, so can't be used. Or have to check if the versions of current and temp are same and then + // take a decision based on that. + val (tempRcVersion, tempConfig, tempPkg, tempRes) = readReleaseConfigFromDir("backupTempDir/$APP_DIR") + if (curRcVersion == tempRcVersion && curConfig == tempConfig && curPkg == tempPkg && curRes == tempRes) { + return backupInternal(stage) + } else { + return backupInternal(BACKUP_STAGES.STARTED.name) + } + } + return backupInternal(if (stage == "" || stage == BACKUP_STAGES.FAILED.name) BACKUP_STAGES.STARTED.name else stage ?: BACKUP_STAGES.STARTED.name) + } + + private fun backupInternal(stage: String): Boolean { + + var internalStage = stage + val fps = otaServices.fileProviderService + if (internalStage == BACKUP_STAGES.STARTED.name) { + OTAUtils.deleteRecursive(fps.getFileFromInternalStorageInternal("$backupTempDir/")) + workspace.writeToSharedPreference(Constants.BACKUP_STAGE, BACKUP_STAGES.STARTED.name) + + val files = fps.listFilesRecursive(APP_DIR) + var tempWriteSuccess = true + files?.forEach { name -> + val fileName = "$APP_DIR/$name" + val data = fps.readFromInternalStorage(fileName) // TODO: readFromFile -> Won't this read from assets? Is reading from assets fine? + tempWriteSuccess = tempWriteSuccess && fps.writeToFile(fps.getFileFromInternalStorageInternal("$backupTempDir/$fileName"), data.toByteArray(), false, false) // "$backupTempDir/$name" is this correct? + } + + if (tempWriteSuccess) { + internalStage = BACKUP_STAGES.COPY_TO_BACKUP_IN_PROGRESS.name + workspace.writeToSharedPreference(Constants.BACKUP_STAGE, BACKUP_STAGES.COPY_TO_BACKUP_IN_PROGRESS.name) + } else { + workspace.writeToSharedPreference(Constants.BACKUP_STAGE, BACKUP_STAGES.FAILED.name) + return false + } + } + + if (internalStage == BACKUP_STAGES.COPY_TO_BACKUP_IN_PROGRESS.name) { + workspace.writeToSharedPreference(Constants.BACKUP_STAGE, BACKUP_STAGES.COPY_TO_BACKUP_IN_PROGRESS.name) + var copySuccess = true + val files = fps.listFilesRecursive("$backupTempDir/$APP_DIR") + files?.forEach { name -> + val fileName = "$APP_DIR/$name" + copySuccess = copySuccess && fps.getFileFromInternalStorageInternal("$backupTempDir/$fileName").renameTo(fps.getFileFromInternalStorageInternal("$backupMainDir/$fileName")) + } + + if (copySuccess) { + workspace.writeToSharedPreference(Constants.BACKUP_STAGE, BACKUP_STAGES.DONE.name) + workspace.writeToSharedPreference(Constants.BACKUP_INPLACE, "true") + } else { + workspace.writeToSharedPreference(Constants.BACKUP_INPLACE, "false") + workspace.writeToSharedPreference(Constants.BACKUP_STAGE, BACKUP_STAGES.FAILED.name) + OTAUtils.deleteRecursive(fps.getFileFromInternalStorageInternal("$backupMainDir/")) + return false + } + } + + Log.d(UpdateTask.TAG, "Created restore point at.") + return true + } + + private fun getRolledBackVersions(): JSONArray { + return try { + JSONArray(workspace.getFromSharedPreference(Constants.BLACKLISTED_VERSIONS, JSONArray().toString())) + } catch (_: JSONException) { + workspace.writeToSharedPreference(Constants.BLACKLISTED_VERSIONS, JSONArray().toString()) + JSONArray() + } + } + + private fun addToRolledBackVersions(versions: JSONObject) { + val storedVersions = getRolledBackVersions() + storedVersions.put(versions) + workspace.writeToSharedPreference(Constants.BLACKLISTED_VERSIONS, storedVersions.toString()) + trackInfo("version added to rolled back versions", JSONObject().put("version added to rolled back versions", versions)) + Log.d(TAG, "Added version to rolled-back versions list $versions") + } + + /** + * Rolls back the package update to the last backed up versions. + * If no back up exists then this function returns false. + * + * @return true if rollback was successful, false otherwise. + */ + fun rollbackOTA(clientId: String): Boolean { + workspace.writeToSharedPreference(Constants.ROLLBACK_IN_PROGRESS, "true") + Log.d(UpdateTask.TAG, "Rolling back update.") + cancelTask(clientId) // Will check this + trackInfo("rollback_initiated", JSONObject().put("rc_version", releaseConfig?.version ?: "").put("config_version", releaseConfig?.config?.version ?: "").put("pkg_version", releaseConfig?.pkg?.version ?: "")) + + val backupStage = workspace.getFromSharedPreference(Constants.BACKUP_STAGE, "") + val backupInplace = workspace.getFromSharedPreference(Constants.BACKUP_INPLACE, "false") + + val fps = otaServices.fileProviderService + if (backupInplace == "false" || backupStage == BACKUP_STAGES.COPY_TO_BACKUP_IN_PROGRESS.name) { + Log.e(UpdateTask.TAG, "No backup found for rollback, so deleting the contents inside app dir") + // Here just delete everything + OTAUtils.deleteRecursive(fps.getFileFromInternalStorage(APP_DIR)) + OTAUtils.deleteRecursive(fps.getFileFromInternalStorage(BACKUP_DIR)) + workspace.writeToSharedPreference(Constants.BACKUP_STAGE, "") + trackInfo("rollback_failed", JSONObject().put("reason", "No backup found so deleting the contents inside app dir")) + return true + } + + // Here have to check if the app and backup has same versions, if yes then have to delete everything. + if (backupInplace == "true") { + val (_, config, pkg, res) = readReleaseConfigFromDir("$backupMainDir/$APP_DIR") + + ReleaseConfig.deSerializeConfig(config).onSuccess { + it + if (it.version == releaseConfig?.config?.version) { + // Now delete the config file in app dir and backup also + fps.getFileFromInternalStorage("$APP_DIR/$CONFIG_FILE_NAME").delete() + fps.getFileFromInternalStorage("$backupMainDir/$APP_DIR/$CONFIG_FILE_NAME").delete() + } + } + + ReleaseConfig.deSerializePackage(pkg).onSuccess { pkgSer -> + if (pkgSer.version == releaseConfig?.pkg?.version) { + // Now delete the pkg file and pkg splits in app dir and backup also + fps.getFileFromInternalStorage("$APP_DIR/$PACKAGE_MANIFEST_FILE_NAME").delete() + fps.getFileFromInternalStorage("$backupMainDir/$APP_DIR/$PACKAGE_MANIFEST_FILE_NAME").delete() + + releaseConfig?.pkg?.filePaths?.map { + it + fps.getFileFromInternalStorage("$APP_DIR/$PACKAGE_DIR_NAME/$it").delete() + fps.getFileFromInternalStorage("$backupMainDir/$APP_DIR/$PACKAGE_DIR_NAME/$it").delete() + } + + } + } + + if (releaseConfig?.resources?.toJSON().toString() == res) { + // Now delete the resource file and resource splits of app dir and backup also + fps.getFileFromInternalStorage("$APP_DIR/$RESOURCES_FILE_NAME").delete() + fps.getFileFromInternalStorage("$backupMainDir/$APP_DIR/$RESOURCES_FILE_NAME").delete() + + releaseConfig?.resources?.filePaths?.map { + it + fps.getFileFromInternalStorage("$APP_DIR/$PACKAGE_DIR_NAME/$it").delete() + fps.getFileFromInternalStorage("$backupMainDir/$APP_DIR/$PACKAGE_DIR_NAME/$it").delete() + } + } + } + + // We don't want to rollback this package as this is being rolled back now. + if (backupStage == BACKUP_STAGES.STARTED.name) { + workspace.writeToSharedPreference(Constants.BACKUP_STAGE, "") + } + var rollbackSuccess = false + val files = fps.listFilesRecursive(backupMainDir) + if (files?.isEmpty() ?: true) { + trackInfo("rollback_failed", JSONObject().put("reason", "Backup folder is empty")) + } else { + files.forEach { name -> + val fileName = "$APP_DIR/$name" + val data = fps.readFromFile("$backupMainDir/$fileName") + rollbackSuccess = fps.writeToFile(fps.getFileFromInternalStorageInternal("$APP_DIR/$name"), data.toByteArray(), false, false) + if (!rollbackSuccess) { + trackInfo("rollback_failed", JSONObject().put("reason", "Not able to write the file $fileName from backup to main dir")) + return@forEach + } + } + } + + if (!rollbackSuccess) { + OTAUtils.deleteRecursive(fps.getFileFromInternalStorage(APP_DIR)) + } + workspace.writeToSharedPreference(Constants.ROLLBACK_IN_PROGRESS, "false") + + if (rollbackSuccess) { + Log.d(UpdateTask.TAG, "Rollback successful") + trackInfo("rollback_success", JSONObject().put("result", "success")) + } + val resContent = releaseConfig?.resources?.toJSON().toString().toByteArray() + val resHash = try { + OTAUtils.SHA256(resContent).toString() + } catch (_: Exception) { + null + } + releaseConfig?.let { rc -> + val json = JSONObject().put("configVersion", rc.config.version).put("pkgVersion", rc.pkg.version) + resHash?.let { + json.put("resHash", it) + } + addToRolledBackVersions(json) + trackInfo("release_blacklisted", json) + } + return rollbackSuccess + } + + fun cancelTask(clientId: String) { + RUNNING_UPDATE_TASKS.remove(clientId)?.cancel() + } + + fun cancelAllUpdateTasks() { + val snapshot = RUNNING_UPDATE_TASKS.entries.toList() + snapshot.forEach { (clientId, task) -> + task.cancel() + RUNNING_UPDATE_TASKS.remove(clientId) + } + } + + companion object { const val TAG = "ApplicationManager" private val CONTEXT_MAP: @@ -525,6 +771,13 @@ class ApplicationManager( } } +enum class BACKUP_STAGES { + STARTED, + COPY_TO_BACKUP_IN_PROGRESS, + DONE, + FAILED +} + interface ReleaseConfigCallback { fun shouldRetry(): Boolean fun getReleaseConfig(fetchFailed: Boolean): String diff --git a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/Constants.kt b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/Constants.kt index bbb7e193..57e72c87 100644 --- a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/Constants.kt +++ b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/Constants.kt @@ -20,6 +20,11 @@ internal object Constants { const val APP_DIR = "app" const val PACKAGE_DIR_NAME = "package" const val RESOURCES_DIR_NAME = "resources" + const val BACKUP_DIR = "airborne-backup" + const val BACKUP_TEMP = "backup_temp" + const val BACKUP_MAIN = "backup" + const val ROLLBACK_IN_PROGRESS = "rollback_in_progress" + const val BLACKLISTED_VERSIONS = "blacklisted_versions" const val RC_VERSION_FILE_NAME = "rc_version.txt" const val PACKAGE_MANIFEST_FILE_NAME = "pkg.json" const val CONFIG_FILE_NAME = "config.json" @@ -40,4 +45,6 @@ internal object Constants { lazy = emptyList() ) val DEFAULT_RESOURCES = ReleaseConfig.ResourceManifest(emptyList()) + const val BACKUP_STAGE = "backup_stage" + const val BACKUP_INPLACE = "backup_inplace" } diff --git a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/UpdateTask.kt b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/UpdateTask.kt index 96f29ba0..77f467cb 100644 --- a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/UpdateTask.kt +++ b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/UpdateTask.kt @@ -41,6 +41,7 @@ import `in`.juspay.airborne.constants.LogLevel import `in`.juspay.airborne.constants.LogSubCategory import okhttp3.Response import okhttp3.ResponseBody +import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import java.io.BufferedReader @@ -70,6 +71,7 @@ internal class UpdateTask( private val releaseConfigUrl: String, private val fileProviderService: FileProviderService, private var localReleaseConfig: ReleaseConfig?, + private val blackListedVersions: JSONArray, private val fileLock: Any, private val tracker: TrackerCallback, private val netUtils: NetUtils, @@ -114,6 +116,8 @@ internal class UpdateTask( // TODO Move to storing in main app dir & remove this var. private var resourceSaveFuture: Future? = null + private var isCancelled = false + init { trackers.add(tracker) val sortedHeaders = (rcHeaders ?: emptyMap()).toSortedMap() @@ -149,6 +153,13 @@ internal class UpdateTask( } } + fun cancel() { + isCancelled = true + packageUpdate?.cancel(true) + resourceUpdate?.futures?.forEach { it.cancel(true) } + Log.d(TAG, "UpdateTask cancelled.") + } + private fun setCurrentResult( version: String? = null, config: ReleaseConfig.Config? = null, @@ -176,18 +187,25 @@ internal class UpdateTask( private fun runInternal() { val fetched = fetchReleaseConfig() - var shouldDownloadCurLazySplits = false + var shouldDownloadCurLazySplits = true if (fetched == null) { // Unable to fetch so exiting. currentResult = UpdateResult.Error.RCFetchError onComplete(Stage.INSTALLING) } else { - updateTimeouts(fetched) + val blCheckRes = checkIfVersionBlacklisted(fetched) + if (!blCheckRes.configBlackListed) { + updateTimeouts(fetched) + } onComplete(Stage.FETCHING_RC) val pupdateStart = System.currentTimeMillis() - packageUpdate = doAsync { downloadPackageUpdate(fetched.pkg) } - resourceUpdate = ResourceUpdateTask(localReleaseConfig?.resources, fetched.resources) - resourceUpdate?.start() + if (!blCheckRes.packageBlackListed) { + packageUpdate = doAsync { downloadPackageUpdate(fetched.pkg) } + } + if (!blCheckRes.resBlackListed) { + resourceUpdate = ResourceUpdateTask(localReleaseConfig?.resources, fetched.resources) + resourceUpdate?.start() + } var updatedConfig: ReleaseConfig.Config? = null if (fetched.version != localReleaseConfig?.version) { if (writeRCVersion(fetched.version)) { @@ -198,7 +216,7 @@ internal class UpdateTask( Log.d(TAG, "RC Version updated.") } } - if (fetched.config.version != localReleaseConfig?.config?.version) { + if (!blCheckRes.configBlackListed && fetched.config.version != localReleaseConfig?.config?.version) { if (writeConfig(fetched.config)) { updatedConfig = fetched.config setCurrentResult(fetched.version, updatedConfig) @@ -214,12 +232,10 @@ internal class UpdateTask( onComplete(Stage.DOWNLOADING_UPDATES) var didPackageUpdate = false var updatedPackage: ReleaseConfig.PackageManifest? = null - if (!updateTimedOut.get()) { + if (presult != null && !updateTimedOut.get()) { Log.d(TAG, "Installing package as updateTimedout is false") val packageInstallFuture = doAsync { - presult?.let { - installPackageUpdate(it, fetched.pkg, pupdateStart) - } + installPackageUpdate(presult, fetched.pkg, pupdateStart) } didPackageUpdate = packageInstallFuture.get() updatedPackage = if (didPackageUpdate) fetched.pkg else null @@ -424,6 +440,34 @@ internal class UpdateTask( return null } + private fun checkIfVersionBlacklisted(releaseConfig: ReleaseConfig): BlackListResult { + var blConfigVersionRes = false + var blPkgVersionRes = false + var blResourRes = false + val curReshHash = try { + OTAUtils.SHA256(releaseConfig.resources.toJSON().toString().toByteArray()).toString() + } catch (_: Exception) { + "" + } + for (i in 0 until blackListedVersions.length()) { + val blVersions = blackListedVersions.getJSONObject(i) + val configVersion = blVersions.optString("configVersion", "-1") + val pkgVersion = blVersions.optString("pkgVersion", "-1") + val resHash = blVersions.optString("resHash", "-1") + + if (!blConfigVersionRes) { + blConfigVersionRes = configVersion == releaseConfig.config.version + } + if (!blPkgVersionRes) { + blPkgVersionRes = pkgVersion == releaseConfig.pkg.version + } + if (!blResourRes) { + blResourRes = resHash == curReshHash + } + } + return BlackListResult(blConfigVersionRes, blPkgVersionRes, blResourRes) + } + private fun installPackageUpdate( update: Update.Package, pkg: ReleaseConfig.PackageManifest, @@ -709,6 +753,10 @@ internal class UpdateTask( Log.d(TAG, "Starting lazy split downloads.") val downloads = splits.map { doAsync { + if (isCancelled) { + Log.d(TAG, "Cancelled, skipping download for ${it.filePath}") + return@doAsync Result.Ok(Unit) + } val result = if (it.isDownloaded == true) { Result.Ok(Unit) } else { @@ -833,6 +881,13 @@ internal class UpdateTask( savePersistentState(state) } + // ------ ROLLBACK ----- +// private fun checkAndCreateDefaultRestorePoint() { +// if (!fileProviderService.getFileFromInternalStorage("$packageBackupDir/default/.keep").exists()) { +// applicationManager.backupPackage("default") +// } +// } + // ----- NETWORK-UTILS ----- private fun downloadFile( tempWriter: TempWriter, @@ -1221,11 +1276,9 @@ internal class UpdateTask( tempWriter: TempWriter ): Future = doAsync { - val dest = - if (fromAirborne) "$APP_DIR/$PACKAGE_DIR_NAME" else "$APP_DIR/$RESOURCES_DIR_NAME" if (tempWriter.copyToMain( resource.filePath, - dest + "$APP_DIR/$PACKAGE_DIR_NAME" ) ) { resource @@ -1285,6 +1338,8 @@ internal class UpdateTask( } } + private class BlackListResult(val configBlackListed: Boolean, val packageBlackListed: Boolean, val resBlackListed: Boolean) + private enum class Stage { INITIALIZING, FETCHING_RC, diff --git a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/services/FileProviderService.java b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/services/FileProviderService.java index 546dce9b..38c7d16c 100644 --- a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/services/FileProviderService.java +++ b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/services/FileProviderService.java @@ -248,16 +248,16 @@ private boolean isFileCached(String fileName) { @SuppressWarnings("UnusedReturnValue") public boolean updateFile(@NonNull Context context, String fileName, byte[] content) { - return writeToFile(context, fileName, content, false); + return writeToFileAtomic(context, fileName, content, false); } public boolean updateFile(String fileName, byte[] content) { - return writeToFile(Workspace.getCtx(), fileName, content, false); + return writeToFileAtomic(Workspace.getCtx(), fileName, content, false); } @SuppressWarnings("UnusedReturnValue") public boolean updateCertificate(@NonNull Context context, String fileName, byte[] content) { - return writeToFile(context, fileName, content, true); + return writeToFileAtomic(context, fileName, content, true); } boolean copyFile(File from, File to) { @@ -294,12 +294,13 @@ boolean copyFile(File from, File to) { } } - private boolean writeToFile(@NonNull Context context, String realFileName, byte[] content, boolean isCertificate) { + private boolean writeToFileAtomic(@NonNull Context context, String realFileName, byte[] content, boolean isCertificate) { deleteFileFromCache(realFileName); - return writeToFile(getFileFromInternalStorageInternal(realFileName), content, isCertificate); + return writeToFile(getFileFromInternalStorageInternal(realFileName), content, isCertificate, true); } - boolean writeToFile(File file, byte[] content, boolean isCertificate) { + // This write file takes care of atomicity by writing the file to a temp file and then renaming that to the original file name. + public boolean writeToFile(File file, byte[] content, boolean isCertificate, boolean writeAtomic) { final TrackerCallback tracker = otaServices.getTrackerCallback(); try { @@ -311,9 +312,12 @@ boolean writeToFile(File file, byte[] content, boolean isCertificate) { content = decodeResponse.getContent(); } - File tempFile = new File(file.getParentFile(), "temp_" + decodedFileName); + File tempFile = new File(file.getParentFile(), writeAtomic ? "temp_" + decodedFileName : decodedFileName); try (FileOutputStream fos = new FileOutputStream(tempFile)) { fos.write(content); + if (!writeAtomic) { + return true; + } return tempFile.renameTo(new File(file.getParentFile(), decodedFileName)); } catch (Exception e) { tracker.trackException(LogCategory.ACTION, LogSubCategory.Action.SYSTEM, Labels.System.FILE_PROVIDER_SERVICE, "Exception writing decrypted js file " + decodedFileName, e); @@ -362,7 +366,7 @@ public File getFileFromInternalStorage(String fileName) { return getFileFromInternalStorageInternal(fileName); } - private File getFileFromInternalStorageInternal(String fileName) { + public File getFileFromInternalStorageInternal(String fileName) { Log.d(LOG_TAG, "Getting file from internal storage. Filename: " + fileName); diff --git a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/services/TempWriter.kt b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/services/TempWriter.kt index 86d89351..d5c074b3 100644 --- a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/services/TempWriter.kt +++ b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/services/TempWriter.kt @@ -42,7 +42,7 @@ class TempWriter internal constructor(s: String, m: FileProviderService.Mode, pr fun write(fileName: String, content: ByteArray): Boolean { val f = File(tempDir, fileName) f.parentFile?.mkdirs() - return fileProviderService.writeToFile(f, content, false) + return fileProviderService.writeToFile(f, content, false, false) } val dirName: String diff --git a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/utils/OTAUtils.kt b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/utils/OTAUtils.kt index 7af261dc..d9583720 100644 --- a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/utils/OTAUtils.kt +++ b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/utils/OTAUtils.kt @@ -55,18 +55,11 @@ object OTAUtils { @JvmStatic @Throws(CertificateException::class) fun validatePinning(chain: Array, validPins: Set): Boolean { - val md: MessageDigest val certChainMsg = StringBuilder() - try { - md = MessageDigest.getInstance("SHA-256") - } catch (e: NoSuchAlgorithmException) { - throw CertificateException("couldn't create digest") - } for (cert in chain) { val publicKey = cert.publicKey.encoded - md.update(publicKey, 0, publicKey.size) - val pin = Base64.encodeToString(md.digest(), Base64.NO_WRAP) + val pin = Base64.encodeToString(SHA256(publicKey), Base64.NO_WRAP) certChainMsg.append(" sha256/").append(pin).append(" : ") .append(cert.subjectDN.toString()).append("\n") return !validPins.contains(pin) @@ -100,4 +93,16 @@ object OTAUtils { } return null } + + @Throws(CertificateException::class) + fun SHA256(content: ByteArray): ByteArray { + val md: MessageDigest + try { + md = MessageDigest.getInstance("SHA-256") + } catch (_: NoSuchAlgorithmException) { + throw CertificateException("couldn't create digest") + } + md.update(content) + return md.digest() + } } diff --git a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/AJPResource.m b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/AJPResource.m index 1922570a..70568b1d 100644 --- a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/AJPResource.m +++ b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/AJPResource.m @@ -30,7 +30,7 @@ - (instancetype)initWithDictionary:(NSDictionary *)dictionary error:(NSError **) _url = [NSURL URLWithString:urlString]; // Parse filePath - NSString *filePath = dictionary[@"file_path"]; + NSString *filePath = dictionary[@"filePath"]; if (![filePath isKindOfClass:[NSString class]]) { if (error) { *error = [NSError errorWithDomain:@"ResourceError" code:402 userInfo:@{NSLocalizedDescriptionKey: @"Invalid filePath"}]; diff --git a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneSwift/Airborne.swift b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneSwift/Airborne.swift index 49de4d5a..6e3400f3 100644 --- a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneSwift/Airborne.swift +++ b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneSwift/Airborne.swift @@ -28,7 +28,7 @@ import AirborneObjC * @return A string identifier for the application namespace. * If not implemented, defaults to "juspay". */ - @objc optional func namespace() -> String + @objc func namespace() -> String /** * Returns the custom bundle for loading local assets and fallback files. @@ -71,7 +71,7 @@ import AirborneObjC * @note Boot completion occurs even if some downloads failed or timed out. * Check the release configuration for actual status. */ - @objc optional func startApp(indexBundleURL: URL?) -> Void + @objc func startApp(indexBundleURL: URL) -> Void /** * Called when significant events occur during the OTA update process. @@ -139,7 +139,7 @@ import AirborneObjC private let releaseConfigURL: String private lazy var namespace: String = { // TODO: Default namespace needs to be confirmed - delegate?.namespace?() ?? "default" + delegate?.namespace() ?? "default" }() private lazy var dimensions: [String: String] = { delegate?.dimensions?() ?? [:] @@ -189,8 +189,11 @@ import AirborneObjC private func startApplicationManager() { self.applicationManager = AJPApplicationManager.getSharedInstance(withWorkspace: self.namespace, delegate: self, logger: self) self.applicationManager?.waitForPackagesAndResources { [weak self] _ in - if let indexBundlePath = self?.getIndexBundlePath() { - self?.delegate?.startApp?(indexBundleURL: indexBundlePath) + + if let indexBundleURL = self?.getIndexBundlePath() { + self?.delegate?.startApp(indexBundleURL: indexBundleURL) + } else { + print("❌ Could not find a valid JS bundle URL") } } } @@ -259,7 +262,7 @@ extension AirborneServices { let indexFilePath = self.applicationManager?.getCurrentApplicationManifest().package.index.filePath, !indexFilePath.isEmpty else { - return bundlePath.url(forResource: "main", withExtension: "jsBundle") ?? bundlePath.bundleURL.appendingPathComponent("main.jsBundle") + return bundlePath.url(forResource: "main", withExtension: "jsbundle") ?? bundlePath.bundleURL.appendingPathComponent("main.jsbundle") // get this from release config } guard From 57f4bb9ef2d5c8902f2d3f92db0bda4190d1862c Mon Sep 17 00:00:00 2001 From: Yaswanth Date: Tue, 4 Nov 2025 12:02:02 +0530 Subject: [PATCH 2/2] feat: backup + rollback for iOS --- .../juspay/airborne/ota/ApplicationManager.kt | 4 +- .../java/in/juspay/airborne/ota/Constants.kt | 2 +- .../AJPApplicationManager.m | 549 +++++++++++++++++- .../Constants/AJPApplicationConstants.h | 10 + .../Constants/AJPApplicationConstants.m | 10 + .../include/AJPApplicationManager.h | 17 + .../Airborne/AirborneSwift/Airborne.swift | 25 + 7 files changed, 613 insertions(+), 4 deletions(-) diff --git a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/ApplicationManager.kt b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/ApplicationManager.kt index 6b0504ff..e7bf5e4b 100644 --- a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/ApplicationManager.kt +++ b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/ApplicationManager.kt @@ -693,13 +693,13 @@ class ApplicationManager( } } - // We don't want to rollback this package as this is being rolled back now. + // We don't want to backup this package as this is being rolled back now. if (backupStage == BACKUP_STAGES.STARTED.name) { workspace.writeToSharedPreference(Constants.BACKUP_STAGE, "") } var rollbackSuccess = false val files = fps.listFilesRecursive(backupMainDir) - if (files?.isEmpty() ?: true) { + if (files == null || files.isEmpty()) { trackInfo("rollback_failed", JSONObject().put("reason", "Backup folder is empty")) } else { files.forEach { name -> diff --git a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/Constants.kt b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/Constants.kt index 57e72c87..5fcbcbd8 100644 --- a/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/Constants.kt +++ b/airborne_sdk_android/airborne/src/main/java/in/juspay/airborne/ota/Constants.kt @@ -20,7 +20,7 @@ internal object Constants { const val APP_DIR = "app" const val PACKAGE_DIR_NAME = "package" const val RESOURCES_DIR_NAME = "resources" - const val BACKUP_DIR = "airborne-backup" + const val BACKUP_DIR = "airborne_backup" const val BACKUP_TEMP = "backup_temp" const val BACKUP_MAIN = "backup" const val ROLLBACK_IN_PROGRESS = "rollback_in_progress" diff --git a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/AJPApplicationManager.m b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/AJPApplicationManager.m index 582de646..c3ea8aff 100644 --- a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/AJPApplicationManager.m +++ b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/AJPApplicationManager.m @@ -22,6 +22,13 @@ typedef NS_ENUM(NSInteger, DownloadStatus) { FAILED }; +typedef NS_ENUM(NSInteger, AJPBackupStage) { + AJPBackupStageStarted, + AJPBackupStageCopyToBackupInProgress, + AJPBackupStageDone, + AJPBackupStageFailed +}; + @implementation AJPDownloadResult - (instancetype) initWithManifest:(AJPApplicationManifest* _Nonnull)releaseConfig result:(NSString* _Nonnull)result error:(NSString* _Nullable)error { @@ -85,6 +92,22 @@ @interface AJPApplicationManager() { @property (nonatomic, strong) AJPFileUtil* fileUtil; @property (nonatomic, strong) AJPRemoteFileUtil* remoteFileUtil; +// Backup and Rollback private methods +- (void)writeToUserDefaults:(NSString *)key value:(NSString *)value; +- (NSString *)readFromUserDefaults:(NSString *)key defaultValue:(NSString *)defaultValue; +- (NSString *)backupStageToString:(AJPBackupStage)stage; +- (AJPBackupStage)stringToBackupStage:(NSString *)stageString; +- (void)completeRollback; +- (void)checkIfBackupPending; +- (NSArray *)readReleaseConfigFromDir:(NSString *)dirPath; +- (BOOL)backupOTA; +- (BOOL)backupInternalWithStage:(AJPBackupStage)stage; +- (NSArray *)getAllFilesInDirectoryRecursively:(NSString *)dirPath; +- (void)deleteDirectoryRecursively:(NSString *)dirPath; +- (NSArray *)getRolledBackVersions; +- (void)addToRolledBackVersions:(NSDictionary *)versionDict; +- (NSString *)calculateResourcesHash:(NSDictionary *)resources; + @end @implementation AJPApplicationManager @@ -255,6 +278,9 @@ - (void)initializeDefaults { _availableLazySplits = [self dictionaryFromResources:self.package.lazy]; _availableResources = [NSMutableDictionary dictionaryWithDictionary:self.resources.resources]; [self.tracker trackInfo:@"init_with_local_config_versions" value:[@{@"package_version":self.package.version, @"config_version":self.config.version} mutableCopy]]; + + // Handle recovery mechanisms + [self completeRollback]; } - (void)handleTempPackageInstallation { @@ -1346,6 +1372,9 @@ - (void)updatePackage:(AJPApplicationPackage *)package didDownloadImportant:(BOO } else { [self.tracker trackInfo:@"package_update_result" value:[@{@"result" : @"FAILED", @"reason" : @"package copy failed", @"time_taken" : [NSNumber numberWithDouble:(([[NSDate date] timeIntervalSince1970] * 1000) - startTime)]}mutableCopy]]; } + + // Check if backup is pending after package update (regardless of success/failure) + [self checkIfBackupPending]; } - (void)updatePackageInTemp:(AJPApplicationPackage *)package { @@ -1928,8 +1957,526 @@ - (NSMutableDictionary *)dictionaryFromResources:(NSArray*)resour for (AJPResource *resource in resources) { dictionary[resource.filePath] = resource; } - + return dictionary; } +#pragma mark - Backup and Rollback Implementation + +- (NSArray *)readReleaseConfigFromDir:(NSString *)dirPath { + NSArray *fileNames = @[APP_CONFIG_DATA_FILE_NAME, APP_PACKAGE_DATA_FILE_NAME, APP_RESOURCES_DATA_FILE_NAME]; + NSMutableArray *contents = [NSMutableArray array]; + + // In iOS, manifest files are stored in JuspayManifests subdirectory + NSString *manifestsPath = [dirPath stringByAppendingPathComponent:JUSPAY_MANIFEST_DIR]; + + for (NSString *fileName in fileNames) { + NSString *content = [self.fileUtil loadFile:fileName folder:manifestsPath withLocalAssets:NO error:nil]; + [contents addObject:content ?: @""]; + } + + return [contents copy]; +} + +- (BOOL)createRestorePoint { + return [self backupOTA]; +} + +- (BOOL)backupOTA { + NSString *currentStage = [self readFromUserDefaults:BACKUP_STAGE_KEY defaultValue:@""]; + + NSString *backupMainPath = [self.fileUtil fullPathInStorageForFilePath:JUSPAY_BACKUP_MAIN_DIR inFolder:JUSPAY_BACKUP_DIR]; + NSString *backupTempPath = [self.fileUtil fullPathInStorageForFilePath:JUSPAY_BACKUP_TEMP_DIR inFolder:JUSPAY_BACKUP_DIR]; + + // Read current and backup release configs to compare + // In iOS, we need to read from the JuspayManifests directory where manifest files are stored + NSString *currentManifestsPath = [self.fileUtil fullPathInStorageForFilePath:@"" inFolder:JUSPAY_MANIFEST_DIR]; + + NSArray *backupConfig = [self readReleaseConfigFromDir:backupMainPath]; + NSArray *currentConfig = [self readReleaseConfigFromDir:currentManifestsPath]; + + // Check if backup is already up to date + if ([currentStage isEqualToString:[self backupStageToString:AJPBackupStageDone]] && + [backupConfig isEqualToArray:currentConfig]) { + [self.tracker trackInfo:@"backup_already_current" value:[@{@"reason": @"Backup is already up to date"} mutableCopy]]; + return YES; + } + + // Handle interrupted backup + if ([currentStage isEqualToString:[self backupStageToString:AJPBackupStageCopyToBackupInProgress]]) { + NSString *backupTempManifestsPath = [backupTempPath stringByAppendingPathComponent:JUSPAY_MANIFEST_DIR]; + NSArray *tempConfig = [self readReleaseConfigFromDir:backupTempManifestsPath]; + + if ([currentConfig isEqualToArray:tempConfig]) { + return [self backupInternalWithStage:AJPBackupStageCopyToBackupInProgress]; + } else { + return [self backupInternalWithStage:AJPBackupStageStarted]; + } + } + + AJPBackupStage stage = AJPBackupStageStarted; + if ([currentStage isEqualToString:@""] || [currentStage isEqualToString:[self backupStageToString:AJPBackupStageFailed]]) { + stage = AJPBackupStageStarted; + } else { + stage = [self stringToBackupStage:currentStage]; + } + + return [self backupInternalWithStage:stage]; +} + +- (BOOL)backupInternalWithStage:(AJPBackupStage)stage { + NSString *backupTempPath = [self.fileUtil fullPathInStorageForFilePath:JUSPAY_BACKUP_TEMP_DIR inFolder:JUSPAY_BACKUP_DIR]; + NSString *backupMainPath = [self.fileUtil fullPathInStorageForFilePath:JUSPAY_BACKUP_MAIN_DIR inFolder:JUSPAY_BACKUP_DIR]; + + AJPBackupStage internalStage = stage; + + // Stage 1: Copy app directory to backup temp + if (internalStage == AJPBackupStageStarted) { + [self.tracker trackInfo:@"backup_started" value:[@{@"stage": @"copying_to_temp"} mutableCopy]]; + + // Clean up existing temp directory + [self deleteDirectoryRecursively:backupTempPath]; + [self writeToUserDefaults:BACKUP_STAGE_KEY value:[self backupStageToString:AJPBackupStageStarted]]; + + // Copy both JuspayManifests and JuspayPackages directories to backup temp + NSString *manifestsPath = [self.fileUtil fullPathInStorageForFilePath:@"" inFolder:JUSPAY_MANIFEST_DIR]; + NSString *packagesPath = [self.fileUtil fullPathInStorageForFilePath:@"" inFolder:JUSPAY_PACKAGE_DIR]; + + BOOL tempCopySuccess = YES; + + // Copy JuspayManifests directory + NSArray *manifestFiles = [self getAllFilesInDirectoryRecursively:manifestsPath]; + for (NSString *relativePath in manifestFiles) { + NSString *sourcePath = [manifestsPath stringByAppendingPathComponent:relativePath]; + NSString *destPath = [backupTempPath stringByAppendingPathComponent:JUSPAY_MANIFEST_DIR]; + destPath = [destPath stringByAppendingPathComponent:relativePath]; + + // Ensure destination directory exists + NSString *destDir = [destPath stringByDeletingLastPathComponent]; + [self.fileUtil createFolderIfDoesNotExist:destDir]; + + // Copy file + NSError *error = nil; + NSData *fileData = [NSData dataWithContentsOfFile:sourcePath options:0 error:&error]; + if (fileData && [fileData writeToFile:destPath options:NSDataWritingAtomic error:&error]) { + // Success + } else { + tempCopySuccess = NO; + [self.tracker trackError:@"backup_manifest_copy_failed" value:[@{@"file": relativePath, @"error": error ? error.localizedDescription : @"Unknown error"} mutableCopy]]; + break; + } + } + + // Copy JuspayPackages directory if manifest copy succeeded + if (tempCopySuccess) { + NSArray *packageFiles = [self getAllFilesInDirectoryRecursively:packagesPath]; + for (NSString *relativePath in packageFiles) { + NSString *sourcePath = [packagesPath stringByAppendingPathComponent:relativePath]; + NSString *destPath = [backupTempPath stringByAppendingPathComponent:JUSPAY_PACKAGE_DIR]; + destPath = [destPath stringByAppendingPathComponent:relativePath]; + + // Ensure destination directory exists + NSString *destDir = [destPath stringByDeletingLastPathComponent]; + [self.fileUtil createFolderIfDoesNotExist:destDir]; + + // Copy file + NSError *error = nil; + NSData *fileData = [NSData dataWithContentsOfFile:sourcePath options:0 error:&error]; + if (fileData && [fileData writeToFile:destPath options:NSDataWritingAtomic error:&error]) { + // Success + } else { + tempCopySuccess = NO; + [self.tracker trackError:@"backup_package_copy_failed" value:[@{@"file": relativePath, @"error": error ? error.localizedDescription : @"Unknown error"} mutableCopy]]; + break; + } + } + } + + if (tempCopySuccess) { + internalStage = AJPBackupStageCopyToBackupInProgress; + [self writeToUserDefaults:BACKUP_STAGE_KEY value:[self backupStageToString:AJPBackupStageCopyToBackupInProgress]]; + } else { + [self writeToUserDefaults:BACKUP_STAGE_KEY value:[self backupStageToString:AJPBackupStageFailed]]; + [self.tracker trackError:@"backup_failed" value:[@{@"stage": @"temp_copy"} mutableCopy]]; + return NO; + } + } + + // Stage 2: Move from temp to main backup + if (internalStage == AJPBackupStageCopyToBackupInProgress) { + [self.tracker trackInfo:@"backup_moving_to_main" value:[@{@"stage": @"moving_to_main"} mutableCopy]]; + [self writeToUserDefaults:BACKUP_STAGE_KEY value:[self backupStageToString:AJPBackupStageCopyToBackupInProgress]]; + + // Clean up existing main backup + [self deleteDirectoryRecursively:backupMainPath]; + + // Move temp to main + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *moveError = nil; + BOOL moveSuccess = [fileManager moveItemAtPath:backupTempPath toPath:backupMainPath error:&moveError]; + + if (moveSuccess) { + [self writeToUserDefaults:BACKUP_STAGE_KEY value:[self backupStageToString:AJPBackupStageDone]]; + [self writeToUserDefaults:BACKUP_INPLACE_KEY value:@"true"]; + [self.tracker trackInfo:@"backup_completed" value:[@{@"result": @"success"} mutableCopy]]; + return YES; + } else { + [self writeToUserDefaults:BACKUP_INPLACE_KEY value:@"false"]; + [self writeToUserDefaults:BACKUP_STAGE_KEY value:[self backupStageToString:AJPBackupStageFailed]]; + [self deleteDirectoryRecursively:backupMainPath]; + [self.tracker trackError:@"backup_failed" value:[@{@"stage": @"move_to_main", @"error": moveError ? moveError.localizedDescription : @"Unknown error"} mutableCopy]]; + return NO; + } + } + + [self.tracker trackInfo:@"backup_completed" value:[@{@"result": @"success"} mutableCopy]]; + return YES; +} + +- (NSArray *)getAllFilesInDirectoryRecursively:(NSString *)dirPath { + NSMutableArray *allFiles = [NSMutableArray array]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + + NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtPath:dirPath]; + for (NSString *relativePath in enumerator) { + NSString *fullPath = [dirPath stringByAppendingPathComponent:relativePath]; + + BOOL isDirectory = NO; + if ([fileManager fileExistsAtPath:fullPath isDirectory:&isDirectory] && !isDirectory) { + [allFiles addObject:relativePath]; + } + } + + return [allFiles copy]; +} + +- (void)deleteDirectoryRecursively:(NSString *)dirPath { + NSFileManager *fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath:dirPath]) { + NSError *error = nil; + [fileManager removeItemAtPath:dirPath error:&error]; + if (error) { + [self.tracker trackError:@"directory_delete_failed" value:[@{@"path": dirPath, @"error": error.localizedDescription} mutableCopy]]; + } + } +} + +#pragma mark - Version Blacklisting + +- (NSArray *)getRolledBackVersions { + NSString *blacklistedJson = [self readFromUserDefaults:BLACKLISTED_VERSIONS_KEY defaultValue:@"[]"]; + NSError *error = nil; + NSData *jsonData = [blacklistedJson dataUsingEncoding:NSUTF8StringEncoding]; + NSArray *versions = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + + if (error || versions == nil) { + [self writeToUserDefaults:BLACKLISTED_VERSIONS_KEY value:@"[]"]; + return @[]; + } + + return versions ?: @[]; +} + +- (void)addToRolledBackVersions:(NSDictionary *)versionDict { + NSMutableArray *storedVersions = [[self getRolledBackVersions] mutableCopy]; + [storedVersions addObject:versionDict]; + + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:storedVersions options:0 error:&error]; + if (!error) { + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + [self writeToUserDefaults:BLACKLISTED_VERSIONS_KEY value:jsonString]; + [self.tracker trackInfo:@"version_blacklisted" value:[@{@"version": versionDict} mutableCopy]]; + } +} + +- (NSString *)calculateResourcesHash:(NSDictionary *)resources { + // Create a consistent JSON representation + NSMutableArray *resourceArray = [NSMutableArray array]; + NSArray *sortedKeys = [[resources allKeys] sortedArrayUsingSelector:@selector(compare:)]; + + for (NSString *key in sortedKeys) { + AJPResource *resource = resources[key]; + [resourceArray addObject:[resource toDictionary]]; + } + + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:resourceArray options:0 error:&error]; + if (error) { + return @""; + } + + // Simple hash calculation (you can replace with SHA256 if needed) + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + return [NSString stringWithFormat:@"%lu", (unsigned long)[jsonString hash]]; +} + +#pragma mark - Rollback Implementation + +- (BOOL)rollbackOTA { + [self writeToUserDefaults:ROLLBACK_IN_PROGRESS_KEY value:@"true"]; + [self.tracker trackInfo:@"rollback_initiated" value:[@{ + @"rc_version": self.config.version ?: @"", + @"config_version": self.config.version ?: @"", + @"pkg_version": self.package.version ?: @"" + } mutableCopy]]; + + NSString *backupStage = [self readFromUserDefaults:BACKUP_STAGE_KEY defaultValue:@""]; + NSString *backupInplace = [self readFromUserDefaults:BACKUP_INPLACE_KEY defaultValue:@"false"]; + + NSString *backupMainPath = [self.fileUtil fullPathInStorageForFilePath:JUSPAY_BACKUP_MAIN_DIR inFolder:JUSPAY_BACKUP_DIR]; + NSString *manifestsPath = [self.fileUtil fullPathInStorageForFilePath:@"" inFolder:JUSPAY_MANIFEST_DIR]; + NSString *packagesPath = [self.fileUtil fullPathInStorageForFilePath:@"" inFolder:JUSPAY_PACKAGE_DIR]; + + // Check if backup is valid + if ([backupInplace isEqualToString:@"false"] || + [backupStage isEqualToString:[self backupStageToString:AJPBackupStageCopyToBackupInProgress]]) { + [self.tracker trackError:@"rollback_failed" value:[@{@"reason": @"No valid backup found, deleting app dir"} mutableCopy]]; + + // Delete everything and clean up backup + [self deleteDirectoryRecursively:manifestsPath]; + [self deleteDirectoryRecursively:packagesPath]; + [self deleteDirectoryRecursively:backupMainPath]; + [self writeToUserDefaults:BACKUP_STAGE_KEY value:@""]; + [self writeToUserDefaults:ROLLBACK_IN_PROGRESS_KEY value:@"false"]; + return YES; // Consider this "successful" cleanup + } + + // Check if current and backup have same versions and delete identical components + if ([backupInplace isEqualToString:@"true"]) { + NSArray *backupConfig = [self readReleaseConfigFromDir:backupMainPath]; + NSString *currentManifestsPath = [self.fileUtil fullPathInStorageForFilePath:@"" inFolder:JUSPAY_MANIFEST_DIR]; + NSArray *currentConfig = [self readReleaseConfigFromDir:currentManifestsPath]; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + + // Compare config versions - only delete if they are identical + NSString *backupConfigContent = backupConfig[0]; + NSString *currentConfigContent = currentConfig[0]; + + if (backupConfigContent && currentConfigContent && + ![backupConfigContent isEqualToString:@""] && ![currentConfigContent isEqualToString:@""] && + [backupConfigContent isEqualToString:currentConfigContent]) { + // Versions are the same, delete both current and backup config files + NSString *currentConfigPath = [self.fileUtil fullPathInStorageForFilePath:APP_CONFIG_DATA_FILE_NAME inFolder:JUSPAY_MANIFEST_DIR]; + NSString *backupConfigPath = [backupMainPath stringByAppendingPathComponent:JUSPAY_MANIFEST_DIR]; + backupConfigPath = [backupConfigPath stringByAppendingPathComponent:APP_CONFIG_DATA_FILE_NAME]; + + [fileManager removeItemAtPath:currentConfigPath error:nil]; + [fileManager removeItemAtPath:backupConfigPath error:nil]; + [self.tracker trackInfo:@"rollback_same_config_deleted" value:[@{@"reason": @"Current and backup config versions are identical"} mutableCopy]]; + } + + // Compare package versions - only delete if they are identical + NSString *backupPackageContent = backupConfig[1]; + NSString *currentPackageContent = currentConfig[1]; + + if (backupPackageContent && currentPackageContent && + ![backupPackageContent isEqualToString:@""] && ![currentPackageContent isEqualToString:@""] && + [backupPackageContent isEqualToString:currentPackageContent]) { + // Versions are the same, delete both current and backup package files + NSString *currentPackagePath = [self.fileUtil fullPathInStorageForFilePath:APP_PACKAGE_DATA_FILE_NAME inFolder:JUSPAY_MANIFEST_DIR]; + NSString *backupPackagePath = [backupMainPath stringByAppendingPathComponent:JUSPAY_MANIFEST_DIR]; + backupPackagePath = [backupPackagePath stringByAppendingPathComponent:APP_PACKAGE_DATA_FILE_NAME]; + + [fileManager removeItemAtPath:currentPackagePath error:nil]; + [fileManager removeItemAtPath:backupPackagePath error:nil]; + + // Also delete package splits since package versions are identical + for (AJPResource *resource in [self.package allSplits]) { + NSString *currentSplitPath = [packagesPath stringByAppendingPathComponent:resource.filePath]; + NSString *backupSplitPath = [backupMainPath stringByAppendingPathComponent:JUSPAY_PACKAGE_DIR]; + backupSplitPath = [backupSplitPath stringByAppendingPathComponent:resource.filePath]; + + [fileManager removeItemAtPath:currentSplitPath error:nil]; + [fileManager removeItemAtPath:backupSplitPath error:nil]; + } + [self.tracker trackInfo:@"rollback_same_package_deleted" value:[@{@"reason": @"Current and backup package versions are identical"} mutableCopy]]; + } + + // Compare resource versions - only delete if they are identical + NSString *backupResourcesContent = backupConfig[2]; + NSString *currentResourcesContent = currentConfig[2]; + + if (backupResourcesContent && currentResourcesContent && + ![backupResourcesContent isEqualToString:@""] && ![currentResourcesContent isEqualToString:@""] && + [backupResourcesContent isEqualToString:currentResourcesContent]) { + // Resources are the same, delete both current and backup resource files + NSString *currentResourcesPath = [self.fileUtil fullPathInStorageForFilePath:APP_RESOURCES_DATA_FILE_NAME inFolder:JUSPAY_MANIFEST_DIR]; + NSString *backupResourcesPath = [backupMainPath stringByAppendingPathComponent:JUSPAY_MANIFEST_DIR]; + backupResourcesPath = [backupResourcesPath stringByAppendingPathComponent:APP_RESOURCES_DATA_FILE_NAME]; + + [fileManager removeItemAtPath:currentResourcesPath error:nil]; + [fileManager removeItemAtPath:backupResourcesPath error:nil]; + + // Also delete resource files since resource versions are identical + for (NSString *key in self.resources.resources) { + AJPResource *resource = self.resources.resources[key]; + NSString *currentResourcePath = [packagesPath stringByAppendingPathComponent:resource.filePath]; + NSString *backupResourcePath = [backupMainPath stringByAppendingPathComponent:JUSPAY_PACKAGE_DIR]; + backupResourcePath = [backupResourcePath stringByAppendingPathComponent:resource.filePath]; + + [fileManager removeItemAtPath:currentResourcePath error:nil]; + [fileManager removeItemAtPath:backupResourcePath error:nil]; + } + [self.tracker trackInfo:@"rollback_same_resources_deleted" value:[@{@"reason": @"Current and backup resource versions are identical"} mutableCopy]]; + } + } + + // We don't want to backup this package as this is being rolled back now. + if ([backupStage isEqualToString:[self backupStageToString:AJPBackupStageStarted]]) { + [self writeToUserDefaults:BACKUP_STAGE_KEY value:@""]; + } + + // Perform the actual rollback + BOOL rollbackSuccess = YES; + NSString *backupManifestsPath = [backupMainPath stringByAppendingPathComponent:JUSPAY_MANIFEST_DIR]; + NSString *backupPackagesPath = [backupMainPath stringByAppendingPathComponent:JUSPAY_PACKAGE_DIR]; + + // Get backup files from both directories + NSArray *backupManifestFiles = [self getAllFilesInDirectoryRecursively:backupManifestsPath]; + NSArray *backupPackageFiles = [self getAllFilesInDirectoryRecursively:backupPackagesPath]; + + if (backupManifestFiles.count == 0 && backupPackageFiles.count == 0) { + [self.tracker trackError:@"rollback_failed" value:[@{@"reason": @"Backup folder is empty"} mutableCopy]]; + rollbackSuccess = NO; + } else { + // Copy manifest files from backup to current JuspayManifests directory + for (NSString *relativePath in backupManifestFiles) { + NSString *backupFilePath = [backupManifestsPath stringByAppendingPathComponent:relativePath]; + NSString *destFilePath = [manifestsPath stringByAppendingPathComponent:relativePath]; + + // Ensure destination directory exists + NSString *destDir = [destFilePath stringByDeletingLastPathComponent]; + [self.fileUtil createFolderIfDoesNotExist:destDir]; + + // Copy file + NSError *error = nil; + NSData *fileData = [NSData dataWithContentsOfFile:backupFilePath options:0 error:&error]; + if (fileData && [fileData writeToFile:destFilePath options:NSDataWritingAtomic error:&error]) { + // Success + } else { + rollbackSuccess = NO; + [self.tracker trackError:@"rollback_manifest_failed" value:[@{ + @"reason": [NSString stringWithFormat:@"Failed to copy manifest file %@", relativePath], + @"error": error ? error.localizedDescription : @"Unknown error" + } mutableCopy]]; + break; + } + } + + // Copy package files from backup to current JuspayPackages directory if manifest copy succeeded + if (rollbackSuccess) { + for (NSString *relativePath in backupPackageFiles) { + NSString *backupFilePath = [backupPackagesPath stringByAppendingPathComponent:relativePath]; + NSString *destFilePath = [packagesPath stringByAppendingPathComponent:relativePath]; + + // Ensure destination directory exists + NSString *destDir = [destFilePath stringByDeletingLastPathComponent]; + [self.fileUtil createFolderIfDoesNotExist:destDir]; + + // Copy file + NSError *error = nil; + NSData *fileData = [NSData dataWithContentsOfFile:backupFilePath options:0 error:&error]; + if (fileData && [fileData writeToFile:destFilePath options:NSDataWritingAtomic error:&error]) { + // Success + } else { + rollbackSuccess = NO; + [self.tracker trackError:@"rollback_package_failed" value:[@{ + @"reason": [NSString stringWithFormat:@"Failed to copy package file %@", relativePath], + @"error": error ? error.localizedDescription : @"Unknown error" + } mutableCopy]]; + break; + } + } + } + } + + // Clean up if rollback failed + if (!rollbackSuccess) { + [self deleteDirectoryRecursively:manifestsPath]; + [self deleteDirectoryRecursively:packagesPath]; + } + + [self writeToUserDefaults:ROLLBACK_IN_PROGRESS_KEY value:@"false"]; + + if (rollbackSuccess) { + [self.tracker trackInfo:@"rollback_success" value:[@{@"result": @"success"} mutableCopy]]; + } + + // Add current version to blacklisted versions + NSString *resourcesHash = [self calculateResourcesHash:self.resources.resources]; + NSDictionary *versionInfo = @{ + @"configVersion": self.config.version ?: @"", + @"pkgVersion": self.package.version ?: @"", + @"resHash": resourcesHash + }; + [self addToRolledBackVersions:versionInfo]; + + return rollbackSuccess; +} + +- (void)completeRollback { + NSString *rollbackInProgress = [self readFromUserDefaults:ROLLBACK_IN_PROGRESS_KEY defaultValue:@"false"]; + if ([rollbackInProgress isEqualToString:@"true"]) { + [self rollbackOTA]; + } +} + +- (void)checkIfBackupPending { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *stage = [self readFromUserDefaults:BACKUP_STAGE_KEY defaultValue:@""]; + if ([stage isEqualToString:@""] || + [stage isEqualToString:[self backupStageToString:AJPBackupStageDone]] || + [stage isEqualToString:[self backupStageToString:AJPBackupStageFailed]]) { + return; + } + [self backupOTA]; + }); +} + +#pragma mark - Persistent Storage Utilities + +- (void)writeToUserDefaults:(NSString *)key value:(NSString *)value { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *workspaceKey = [NSString stringWithFormat:@"%@_%@", self.workspace, key]; + [defaults setObject:value forKey:workspaceKey]; + [defaults synchronize]; +} + +- (NSString *)readFromUserDefaults:(NSString *)key defaultValue:(NSString *)defaultValue { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *workspaceKey = [NSString stringWithFormat:@"%@_%@", self.workspace, key]; + NSString *value = [defaults stringForKey:workspaceKey]; + return value ? value : defaultValue; +} + +- (NSString *)backupStageToString:(AJPBackupStage)stage { + switch (stage) { + case AJPBackupStageStarted: + return @"STARTED"; + case AJPBackupStageCopyToBackupInProgress: + return @"COPY_TO_BACKUP_IN_PROGRESS"; + case AJPBackupStageDone: + return @"DONE"; + case AJPBackupStageFailed: + return @"FAILED"; + default: + return @""; + } +} + +- (AJPBackupStage)stringToBackupStage:(NSString *)stageString { + if ([stageString isEqualToString:@"STARTED"]) { + return AJPBackupStageStarted; + } else if ([stageString isEqualToString:@"COPY_TO_BACKUP_IN_PROGRESS"]) { + return AJPBackupStageCopyToBackupInProgress; + } else if ([stageString isEqualToString:@"DONE"]) { + return AJPBackupStageDone; + } else if ([stageString isEqualToString:@"FAILED"]) { + return AJPBackupStageFailed; + } + return AJPBackupStageFailed; // Default to failed for unknown states +} + @end diff --git a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/Constants/AJPApplicationConstants.h b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/Constants/AJPApplicationConstants.h index dc34e407..2bcdc36c 100644 --- a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/Constants/AJPApplicationConstants.h +++ b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/Constants/AJPApplicationConstants.h @@ -39,6 +39,16 @@ extern NSString *const LAZY_PACKAGE_NOTIFICATION; extern NSString *const APPL_MANAGER_SUB_CAT; +// Backup and Rollback Constants +extern NSString *const JUSPAY_BACKUP_DIR; +extern NSString *const JUSPAY_BACKUP_TEMP_DIR; +extern NSString *const JUSPAY_BACKUP_MAIN_DIR; + +extern NSString *const BACKUP_STAGE_KEY; +extern NSString *const BACKUP_INPLACE_KEY; +extern NSString *const ROLLBACK_IN_PROGRESS_KEY; +extern NSString *const BLACKLISTED_VERSIONS_KEY; + @end #endif /* AJPApplicationConstants_h */ diff --git a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/Constants/AJPApplicationConstants.m b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/Constants/AJPApplicationConstants.m index 5c74b386..51caf3be 100644 --- a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/Constants/AJPApplicationConstants.m +++ b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/ApplicationManager/Constants/AJPApplicationConstants.m @@ -37,4 +37,14 @@ @implementation AJPApplicationConstants NSString *const APPL_MANAGER_SUB_CAT = @"hyperota"; +// Backup and Rollback Constants +NSString *const JUSPAY_BACKUP_DIR = @"JuspayBackup"; +NSString *const JUSPAY_BACKUP_TEMP_DIR = @"backup_temp"; +NSString *const JUSPAY_BACKUP_MAIN_DIR = @"backup"; + +NSString *const BACKUP_STAGE_KEY = @"backup_stage"; +NSString *const BACKUP_INPLACE_KEY = @"backup_inplace"; +NSString *const ROLLBACK_IN_PROGRESS_KEY = @"rollback_in_progress"; +NSString *const BLACKLISTED_VERSIONS_KEY = @"blacklisted_versions"; + @end diff --git a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/include/AJPApplicationManager.h b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/include/AJPApplicationManager.h index f86494ac..75fb8f41 100644 --- a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/include/AJPApplicationManager.h +++ b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneObjC/include/AJPApplicationManager.h @@ -175,6 +175,23 @@ NS_ASSUME_NONNULL_BEGIN */ - (NSString *)getPathForPackageFile:(NSString *)fileName; +/** + * Creates a restore point for the current OTA state. + * This method backs up the current application state so it can be restored later if needed. + * + * @return YES if the backup was created successfully, NO otherwise. + */ +- (BOOL)createRestorePoint; + +/** + * Rolls back the OTA update to the last backed up version. + * This method restores the application to the previous state from the backup. + * The current version will be added to the blacklisted versions to prevent re-download. + * + * @return YES if the rollback was successful, NO otherwise. + */ +- (BOOL)rollbackOTA; + @end NS_ASSUME_NONNULL_END diff --git a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneSwift/Airborne.swift b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneSwift/Airborne.swift index 6e3400f3..4a2369b6 100644 --- a/airborne_sdk_iOS/hyper-ota/Airborne/AirborneSwift/Airborne.swift +++ b/airborne_sdk_iOS/hyper-ota/Airborne/AirborneSwift/Airborne.swift @@ -329,6 +329,31 @@ extension AirborneServices { @objc public func getFileContent(atPath path: String) -> String? { return applicationManager?.readPackageFile(path) } + + /** + * Creates a restore point for the current OTA state. + * + * This method backs up the current application state so it can be restored later if needed. + * The backup includes all packages, resources, and configuration files. + * + * @return true if the backup was created successfully, false otherwise + */ + @objc public func createRestorePoint() -> Bool { + return applicationManager?.createRestorePoint() ?? false + } + + /** + * Rolls back the OTA update to the last backed up version. + * + * This method restores the application to the previous state from the backup. + * The current version will be added to the blacklisted versions to prevent re-download. + * If no backup exists, the current app directory will be cleaned up. + * + * @return true if the rollback was successful, false otherwise + */ + @objc public func rollbackOTA() -> Bool { + return applicationManager?.rollbackOTA() ?? false + } } // MARK: - AJPApplicationManagerDelegate Conformance