diff --git a/IonicPortals/build.gradle.kts b/IonicPortals/build.gradle.kts index 119a5e6..0e3e1e1 100644 --- a/IonicPortals/build.gradle.kts +++ b/IonicPortals/build.gradle.kts @@ -35,6 +35,9 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + kotlinOptions { + jvmTarget = "17" + } publishing { singleVariant("release") } @@ -51,6 +54,7 @@ dependencies { api("com.capacitorjs:core:[8.0.0,9.0.0)") compileOnly("io.ionic:liveupdates:0.5.5") + compileOnly("io.ionic:live-update-provider:LOCAL-SNAPSHOT") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") implementation("androidx.core:core-ktx:1.15.0") diff --git a/IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt b/IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt index f5f7abe..e5306b4 100644 --- a/IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt +++ b/IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt @@ -1,9 +1,16 @@ package io.ionic.portals import android.content.Context +import android.util.Log import com.getcapacitor.Plugin import io.ionic.liveupdates.LiveUpdate import io.ionic.liveupdates.LiveUpdateManager +import io.ionic.liveupdateprovider.LiveUpdateError +import io.ionic.liveupdateprovider.ProviderSyncCallback +import io.ionic.liveupdateprovider.ProviderSyncResult +import io.ionic.liveupdateprovider.LiveUpdateProviderManager +import io.ionic.liveupdateprovider.ProviderSyncError + /** * A class representing a Portal that contains information about the web content to load and any @@ -85,6 +92,11 @@ class Portal(val name: String) { } } + /** + * A LiveUpdate manager, if live updates is being used. + */ + var liveUpdateManager: LiveUpdateProviderManager? = null + /** * Whether to run a live update sync when the portal is added to the manager. */ @@ -309,6 +321,7 @@ class PortalBuilder(val name: String) { private var portalFragmentType: Class = PortalFragment::class.java private var onCreate: (portal: Portal) -> Unit = {} private var liveUpdateConfig: LiveUpdate? = null + private var liveUpdateManager: LiveUpdateProviderManager? = null private var devMode: Boolean = true internal constructor(name: String, onCreate: (portal: Portal) -> Unit) : this(name) { @@ -555,8 +568,64 @@ class PortalBuilder(val name: String) { LiveUpdateManager.initialize(context) LiveUpdateManager.cleanVersions(context, liveUpdateConfig.appId) LiveUpdateManager.addLiveUpdateInstance(context, liveUpdateConfig) - if (updateOnAppLoad) { + + if (!updateOnAppLoad) return this + + // old way if no manager defined + if (this.liveUpdateManager == null) { LiveUpdateManager.sync(context, arrayOf(liveUpdateConfig.appId)) + return this + } + + this.liveUpdateManager!!.sync( + callback = object : ProviderSyncCallback { + override fun onSuccess(result: ProviderSyncResult) { + Log.d("PortalBuilder", "Live Update sync complete. Latest app dir: ${liveUpdateManager?.latestAppDirectory}") + } + + override fun onFailure(error: ProviderSyncError) { + Log.e("PortalBuilder", "Live Update sync failed: ${error.message}") + } + } + + ) + return this + } + + /** + * Set a custom [LiveUpdateManager] instance to be used with the Portal. + * + * Example usage (kotlin): + * ```kotlin + * val liveUpdateManager = LiveUpdateManager() + * builder = builder.setLiveUpdateManager(liveUpdateManager) + * ``` + * + * Example usage (java): + * ```java + * LiveUpdateManager liveUpdateManager = new LiveUpdateManager(); + * builder = builder.setLiveUpdateManager(liveUpdateManager); + * ``` + * + * @param liveUpdateManager a custom LiveUpdateManager instance + * @return the instance of the PortalBuilder with the LiveUpdateManager set + */ + @JvmOverloads + fun setLiveUpdateManager(context: Context, liveUpdatesManager: LiveUpdateProviderManager, updateOnAppLoad: Boolean = true): PortalBuilder { + this.liveUpdateManager = liveUpdatesManager + if (updateOnAppLoad) { + this.liveUpdateManager?.sync( + callback = object : ProviderSyncCallback { + override fun onSuccess(result: ProviderSyncResult) { + Log.d("TestApplication", "Live Update sync complete. Latest app dir: ${liveUpdatesManager.latestAppDirectory}") + } + + override fun onFailure(error: ProviderSyncError) { + Log.e("TestApplication", "Live Update sync failed: ${error.message}") + } + } + + ) } return this } @@ -598,9 +667,9 @@ class PortalBuilder(val name: String) { portal.initialContext = this.initialContext portal.portalFragmentType = this.portalFragmentType portal.liveUpdateConfig = this.liveUpdateConfig + portal.liveUpdateManager = this.liveUpdateManager portal.devMode = this.devMode onCreate(portal) return portal } - } diff --git a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalFragment.kt b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalFragment.kt index 2b73fdb..08d18c7 100644 --- a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalFragment.kt +++ b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalFragment.kt @@ -271,7 +271,7 @@ open class PortalFragment : Fragment { */ fun reload() { if(portal?.liveUpdateConfig != null) { - val latestLiveUpdateFiles = LiveUpdateManager.getLatestAppDirectory(requireContext(), portal?.liveUpdateConfig?.appId!!) + val latestLiveUpdateFiles = portal?.liveUpdatesManager?.latestAppDirectory ?: LiveUpdateManager.getLatestAppDirectory(requireContext(), portal?.liveUpdateConfig?.appId!!) if (latestLiveUpdateFiles != null) { if (liveUpdateFiles == null || liveUpdateFiles!!.path != latestLiveUpdateFiles.path) { liveUpdateFiles = latestLiveUpdateFiles @@ -285,7 +285,6 @@ open class PortalFragment : Fragment { bridge?.setServerAssetPath(portal?.startDir!!) } } - // Reload the bridge to the existing start url bridge?.reload() } @@ -327,7 +326,7 @@ open class PortalFragment : Fragment { .addWebViewListeners(webViewListeners) if (portal?.liveUpdateConfig != null) { - liveUpdateFiles = LiveUpdateManager.getLatestAppDirectory(requireContext(), portal?.liveUpdateConfig?.appId!!) + liveUpdateFiles = portal?.liveUpdatesManager?.latestAppDirectory ?: LiveUpdateManager.getLatestAppDirectory(requireContext(), portal?.liveUpdateConfig?.appId!!) bridgeBuilder = if (liveUpdateFiles != null) { if (config == null) { val configFile = File(liveUpdateFiles!!.path + "/capacitor.config.json") @@ -516,4 +515,4 @@ open class PortalFragment : Fragment { } } } -} \ No newline at end of file +} diff --git a/TestApp/build.gradle.kts b/TestApp/build.gradle.kts index 886004a..abed446 100644 --- a/TestApp/build.gradle.kts +++ b/TestApp/build.gradle.kts @@ -52,6 +52,8 @@ androidComponents { } dependencies { + implementation("io.ionic:live-updates-provider:LOCAL-SNAPSHOT") + implementation(project(":IonicPortals")) implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.fragment:fragment-ktx:1.6.2") diff --git a/TestApp/src/main/java/io/ionic/portals/testapp/TestApplication.kt b/TestApp/src/main/java/io/ionic/portals/testapp/TestApplication.kt index a3a206e..d3196ef 100644 --- a/TestApp/src/main/java/io/ionic/portals/testapp/TestApplication.kt +++ b/TestApp/src/main/java/io/ionic/portals/testapp/TestApplication.kt @@ -1,7 +1,97 @@ package io.ionic.portals.testapp import android.app.Application +import android.content.Context +import android.util.Log + +import io.ionic.liveupdateprovider.LiveUpdateError.InvalidConfiguration +import io.ionic.liveupdateprovider.LiveUpdateError.SyncFailed +import io.ionic.liveupdateprovider.LiveUpdateManager +import io.ionic.liveupdateprovider.LiveUpdateProvider +import io.ionic.liveupdateprovider.LiveUpdateProviderRegistry +import io.ionic.liveupdateprovider.SyncCallback +import io.ionic.liveupdateprovider.SyncResult import io.ionic.portals.PortalManager +import java.io.File + + +/** + * Mock implementation of LiveUpdateManager for testing purposes. + * Allows testing config parsing and sync behavior without actual network requests. + */ +internal class MockLiveUpdateManager( + private val appId: String?, + private val channel: String?, + private val latestAppDir: File?, + private val shouldFail: Boolean, + private val failureDetails: String, + private val didUpdate: Boolean +) : LiveUpdateManager { + override fun sync(callback: SyncCallback?) { + // Simulate async behavior with a small delay + Thread(Runnable { + try { + Thread.sleep(5000) + } catch (e: InterruptedException) { + e.printStackTrace() + } + if (this.shouldFail) { + val error = SyncFailed(this.failureDetails, null) + callback?.onError(error) + } else { + val result = object : SyncResult { + override val didUpdate: Boolean = this@MockLiveUpdateManager.didUpdate + } + callback?.onComplete(result) + } + }).start() + } + + override val latestAppDirectory: File? + get() = this.latestAppDir +} + + +class MockLiveUpdateProvider(override val id: String) : LiveUpdateProvider { + @Throws(InvalidConfiguration::class) + override fun createManager(context: Context, config: Map?): LiveUpdateManager { + + val data: Map = config ?: emptyMap() + var shouldFail = false + val shouldFailObj = data["shouldFail"] + if (shouldFailObj is Boolean) { + shouldFail = shouldFailObj + } + + var failureDetails = "Mock sync failed" + val failureDetailsObj = data["failureDetails"] + if (failureDetailsObj is String) { + failureDetails = failureDetailsObj + } + + var didUpdate = false + val didUpdateObj = data["didUpdate"] + if (didUpdateObj is Boolean) { + didUpdate = didUpdateObj + } + + // For testing purposes, we can point to a static directory that simulates the latest app version. + // This was gotten from the logs after a successful sync with the real provider + val filePath = + "/data/user/0/io.ionic.portals.ecommercewebapp/files/ionic_apps/3fde24f8/5966bde5-da2e-4b40-8487-2b0fef7c458b" + val latestAppDir = File(filePath) + + return MockLiveUpdateManager( + data["appId"] as? String, + data["channel"] as? String, + latestAppDir, // latestAppDir + shouldFail, + failureDetails, + didUpdate + ) + } +} + class TestApplication: Application() { @@ -9,6 +99,45 @@ class TestApplication: Application() { super.onCreate() PortalManager.register(BuildConfig.PORTALS_KEY) - PortalManager.newPortal("testportal").create() + Log.d("TestApplication", "Registered portal with key: ${BuildConfig.PORTALS_KEY}") + val portalBuilder = PortalManager.newPortal("testportal") + + + + // Register provider + LiveUpdateProviderRegistry.register(MockLiveUpdateProvider("mock")) + + // Resolve the provider where you want in the app + val provider = LiveUpdateProviderRegistry.resolve("mock") + if (provider == null) { + Log.e("TestApplication", "Failed to register MockLiveUpdateProvider") + } else { + Log.d("TestApplication", "Successfully registered MockLiveUpdateProvider with ID: ${provider.id}") + } + + // create the 3rd party manager + val manager = provider?.createManager( + this, + mapOf( + "appId" to "testAppId", + "channel" to "testChannel", + "shouldFail" to false, + "failureDetails" to "Simulated sync failure", + "didUpdate" to true, + "endpoint" to "https://cloud.provider.io", + "apiKey" to "" + ) + ) + if (manager == null) { + Log.e("TestApplication", "Failed to create LiveUpdateManager from MockLiveUpdateProvider") + } else { + Log.d("TestApplication", "Successfully created LiveUpdateManager from MockLiveUpdateProvider") + + + // set the 3rd party manager + portalBuilder.setLiveUpdateManager(this.applicationContext, manager); + } + + val portal = portalBuilder.create() } } \ No newline at end of file diff --git a/TestAppCompose/build.gradle.kts b/TestAppCompose/build.gradle.kts index 58535ca..674cadb 100644 --- a/TestAppCompose/build.gradle.kts +++ b/TestAppCompose/build.gradle.kts @@ -40,6 +40,9 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } + kotlinOptions { + jvmTarget = "17" + } buildFeatures { compose = true } diff --git a/build.gradle.kts b/build.gradle.kts index 6e199b8..ef90aab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,11 @@ plugins { id("org.jetbrains.dokka") version "2.0.0" - id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" + id("org.jetbrains.kotlin.plugin.serialization") version "2.1.20" } buildscript { - val kotlinVersion = "2.2.20" + //val kotlinVersion = "1.9.25" + val kotlinVersion = "2.1.0" extra.apply { set("kotlinVersion", kotlinVersion) } @@ -22,9 +23,11 @@ buildscript { classpath("io.github.gradle-nexus:publish-plugin:1.1.0") } + classpath("org.jetbrains.dokka:dokka-base:1.7.20") classpath("com.android.tools.build:gradle:8.13.0") classpath("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + classpath("org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlinVersion") } } @@ -35,6 +38,7 @@ if (System.getenv("PORTALS_PUBLISH") == "true") { allprojects { repositories { + mavenLocal() google() mavenCentral() } diff --git a/package.json b/package.json index 28dd58f..533ed24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ionic-portals-android", - "version": "0.13.0", + "version": "0.13.0-rn.1", "description": "Ionic Portals", "homepage": "https://ionic.io/portals", "author": "Ionic Team (https://ionic.io)", diff --git a/settings.gradle b/settings.gradle index 360dcf8..72ae5a2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,11 +5,11 @@ pluginManagement { mavenCentral() } plugins { - id 'org.jetbrains.kotlin.plugin.serialization' version "2.2.20" - id 'org.jetbrains.kotlin.plugin.compose' version "2.2.20" + id 'org.jetbrains.kotlin.plugin.serialization' version "2.1.20" + id 'org.jetbrains.kotlin.plugin.compose' version "2.1.20" id 'com.android.application' version '8.13.0' id 'com.android.library' version '8.13.0' - id 'org.jetbrains.kotlin.android' version '2.2.20' + id 'org.jetbrains.kotlin.android' version '2.1.20' } }