Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions IonicPortals/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ android {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
publishing {
singleVariant("release")
}
Expand All @@ -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-updates-provider:LOCAL-SNAPSHOT")

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("androidx.core:core-ktx:1.15.0")
Expand Down
71 changes: 70 additions & 1 deletion IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
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.SyncCallback
import io.ionic.liveupdateprovider.SyncResult
import io.ionic.liveupdateprovider.LiveUpdateManager as ProviderLiveUpdateManager


/**
* A class representing a Portal that contains information about the web content to load and any
Expand Down Expand Up @@ -85,6 +91,11 @@ class Portal(val name: String) {
}
}

/**
* A LiveUpdate manager, if live updates is being used.
*/
var liveUpdatesManager: ProviderLiveUpdateManager? = null

/**
* Whether to run a live update sync when the portal is added to the manager.
*/
Expand Down Expand Up @@ -309,6 +320,7 @@ class PortalBuilder(val name: String) {
private var portalFragmentType: Class<out PortalFragment?> = PortalFragment::class.java
private var onCreate: (portal: Portal) -> Unit = {}
private var liveUpdateConfig: LiveUpdate? = null
private var liveUpdatesManager: ProviderLiveUpdateManager? = null
private var devMode: Boolean = true

internal constructor(name: String, onCreate: (portal: Portal) -> Unit) : this(name) {
Expand Down Expand Up @@ -555,8 +567,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.liveUpdatesManager == null) {
LiveUpdateManager.sync(context, arrayOf(liveUpdateConfig.appId))
return this
}

this.liveUpdatesManager!!.sync(
callback = object : SyncCallback {
override fun onComplete(result: SyncResult) {
Log.d("PortalBuilder", "Live Update sync complete. Did update: ${result.didUpdate}, latest app dir: ${liveUpdatesManager?.latestAppDirectory}")
}

override fun onError(error: LiveUpdateError.SyncFailed) {
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: ProviderLiveUpdateManager, updateOnAppLoad: Boolean = true): PortalBuilder {
this.liveUpdatesManager = liveUpdatesManager
if (updateOnAppLoad) {
this.liveUpdatesManager?.sync(
callback = object : SyncCallback {
override fun onComplete(result: SyncResult) {
Log.d("TestApplication", "Live Update sync complete. Did update: ${result.didUpdate}, latest app dir: ${liveUpdatesManager.latestAppDirectory}")
}

override fun onError(error: LiveUpdateError.SyncFailed) {
Log.e("TestApplication", "Live Update sync failed: ${error.message}")
}
}

)
}
return this
}
Expand Down Expand Up @@ -598,6 +666,7 @@ class PortalBuilder(val name: String) {
portal.initialContext = this.initialContext
portal.portalFragmentType = this.portalFragmentType
portal.liveUpdateConfig = this.liveUpdateConfig
portal.liveUpdatesManager = this.liveUpdatesManager
portal.devMode = this.devMode
onCreate(portal)
return portal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -285,7 +285,6 @@ open class PortalFragment : Fragment {
bridge?.setServerAssetPath(portal?.startDir!!)
}
}

// Reload the bridge to the existing start url
bridge?.reload()
}
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -516,4 +515,4 @@ open class PortalFragment : Fragment {
}
}
}
}
}
2 changes: 2 additions & 0 deletions TestApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
131 changes: 130 additions & 1 deletion TestApp/src/main/java/io/ionic/portals/testapp/TestApplication.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,143 @@
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<String, Any>?): LiveUpdateManager {

val data: Map<String, Any> = 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() {

override fun onCreate() {
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 "<PROVIDER_API_KEY>"
)
)
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()
}
}
3 changes: 3 additions & 0 deletions TestAppCompose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ android {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
Expand Down
8 changes: 6 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
}
Expand All @@ -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")
}
}

Expand All @@ -35,6 +38,7 @@ if (System.getenv("PORTALS_PUBLISH") == "true") {

allprojects {
repositories {
mavenLocal()
google()
mavenCentral()
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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 <hi@ionic.io> (https://ionic.io)",
Expand Down
6 changes: 3 additions & 3 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}

Expand Down
Loading