Skip to content
5 changes: 2 additions & 3 deletions IonicPortals/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,12 @@ dependencies {
implementation(kotlin("reflect"))

api("com.capacitorjs:core:[8.0.0,9.0.0)")
api("io.ionic:liveupdateprovider:1.0.0")
compileOnly("io.ionic:liveupdates:0.5.5")

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
implementation("androidx.core:core-ktx:1.15.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.3")
implementation("androidx.fragment:fragment-ktx:1.8.5")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ object DevConfiguration {
assetManager.open("$portalDirName/$urlFileName").bufferedReader().use {
it.readText()
}
} catch (e: Exception) {
} catch (_: Exception) {
null
}

Expand All @@ -31,7 +31,7 @@ object DevConfiguration {
assetManager.open("$generalDirName/$urlFileName").bufferedReader().use {
it.readText()
}
} catch (e: Exception) {
} catch (_: Exception) {
null
}
}
Expand All @@ -50,15 +50,15 @@ object DevConfiguration {
var serverConfig = try {
val configFile = context.assets.open("$portalDirName/$capConfigFileName")
CapConfig.loadFromAssets(context, portalDirName)
} catch (e: Exception) {
} catch (_: Exception) {
null
}

if (serverConfig == null) {
serverConfig = try {
val configFile = context.assets.open("$generalDirName/$capConfigFileName")
CapConfig.loadFromAssets(context, generalDirName)
} catch (e: Exception) {
} catch (_: Exception) {
null
}
}
Expand Down
138 changes: 117 additions & 21 deletions IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@ package io.ionic.portals

import android.content.Context
import com.getcapacitor.Plugin
import io.ionic.liveupdateprovider.ProviderManager
import io.ionic.liveupdateprovider.ProviderSyncResult
import io.ionic.liveupdates.LiveUpdate
import io.ionic.liveupdates.LiveUpdateManager
import java.io.File
import java.util.concurrent.CompletableFuture
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.future

/**
* A class representing a Portal that contains information about the web content to load and any
Expand Down Expand Up @@ -65,30 +72,96 @@ class Portal(val name: String) {
* if this value is not set.
*/
var startDir: String = ""
get() = if (field.isEmpty()) name else field
get() = field.ifEmpty { name }

/**
* If the Portal should be loaded in development mode and look for a server URL.
*/
var devMode: Boolean = true

/**
* A LiveUpdate config, if live updates is being used.
* The live update source for a [Portal].
*/
var liveUpdateConfig: LiveUpdate? = null
sealed class LiveUpdateSource {
/**
* Uses Ionic Live Updates to sync and locate the latest web application assets.
*/
data class Ionic(val liveUpdateConfig: LiveUpdate) : LiveUpdateSource()

/**
* Uses an external live update provider to sync and locate the latest web application assets.
*/
data class Provider(val manager: ProviderManager) : LiveUpdateSource()
}

/**
* The live update source for this Portal — [LiveUpdateSource.Ionic] for Ionic Live Updates, or
* [LiveUpdateSource.Provider] for an external provider built with the Live Update Provider SDK.
*/
var liveUpdateSource: LiveUpdateSource? = null
set(value) {
field = value
if (value != null) {
if(value.assetPath == null) {
value.assetPath = this.startDir
}
if (value is LiveUpdateSource.Ionic && value.liveUpdateConfig.assetPath == null) {
value.liveUpdateConfig.assetPath = this.startDir
}
field = value
}

/**
* Whether to run a live update sync when the portal is added to the manager.
* The directory of the latest synced web application assets for this Portal.
*/
fun latestAppDirectory(context: Context): File? {
return when (val source = liveUpdateSource) {
is LiveUpdateSource.Ionic -> LiveUpdateManager.getLatestAppDirectory(context, source.liveUpdateConfig.appId)
is LiveUpdateSource.Provider -> source.manager.latestAppDirectory
null -> null
}
}

/**
* Syncs the external live update provider source if present.
*
* Example usage (kotlin):
* ```kotlin
* val result = portal.syncProvider()
* ```
*
* This is a suspend function and can't be called directly from Java — use [syncProviderAsync] instead.
*
* @return the result of the synchronization operation.
* @throws LiveUpdateNotConfigured if this Portal has no [LiveUpdateSource.Provider] configured.
*/
suspend fun syncProvider(): ProviderSyncResult? {
val source = liveUpdateSource as? LiveUpdateSource.Provider ?: throw LiveUpdateNotConfigured()
return source.manager.sync()
}

/**
* Syncs the external live update provider source if present, returning a [CompletableFuture]
* instead of suspending. This is the Java-friendly counterpart to [syncProvider].
*
* Example usage (java):
* ```java
* portal.syncProviderAsync().thenAccept(result -> {
* // handle result
* }).exceptionally(error -> {
* // handle error (including LiveUpdateNotConfigured)
* return null;
* });
* ```
*
* Kotlin callers should prefer [syncProvider] directly; use this only if you specifically need a
* [CompletableFuture], e.g. for interop with existing Future-based code.
*
* @return a [CompletableFuture] completed with the result of the synchronization operation,
* or completed exceptionally if the sync fails.
*/
fun syncProviderAsync(): CompletableFuture<ProviderSyncResult?> = CoroutineScope(Dispatchers.IO).future { syncProvider() }

/**
* Thrown when a live update sync is requested but the required live update source is not
* present on the [Portal].
*/
var liveUpdateOnAppLoad: Boolean = true
class LiveUpdateNotConfigured : Exception("The requested live update source is not configured for this Portal.")

/**
* Add a Capacitor [Plugin] to be loaded with this Portal.
Expand Down Expand Up @@ -308,7 +381,7 @@ class PortalBuilder(val name: String) {
private var initialContext: Any? = null
private var portalFragmentType: Class<out PortalFragment?> = PortalFragment::class.java
private var onCreate: (portal: Portal) -> Unit = {}
private var liveUpdateConfig: LiveUpdate? = null
private var liveUpdateSource: Portal.LiveUpdateSource? = null
private var devMode: Boolean = true

internal constructor(name: String, onCreate: (portal: Portal) -> Unit) : this(name) {
Expand Down Expand Up @@ -398,7 +471,7 @@ class PortalBuilder(val name: String) {
* @return the instance of the PortalBuilder with the Asset Map added
*/
fun addAssetMap(assetMap: AssetMap): PortalBuilder {
assetMaps.put(assetMap.getAssetPath(), assetMap)
assetMaps[assetMap.getAssetPath()] = assetMap
return this
}

Expand Down Expand Up @@ -526,28 +599,28 @@ class PortalBuilder(val name: String) {
}

/**
* Set the [LiveUpdate] config if using the Live Updates SDK with Portals.
* Set the [LiveUpdate] config if using Ionic Live Updates with Portals.
*
* Example usage (kotlin):
* ```kotlin
* val liveUpdateConfig = LiveUpdate("appId", "production")
* builder = builder.setLiveUpdateConfig(liveUpdateConfig)
* builder = builder.setLiveUpdateConfig(context, liveUpdateConfig)
* ```
*
* Example usage (java):
* ```java
* LiveUpdate liveUpdateConfig = new LiveUpdate("appId", "production");
* builder = builder.setLiveUpdateConfig(liveUpdateConfig);
* builder = builder.setLiveUpdateConfig(context, liveUpdateConfig);
* ```
*
* @param context the Android [Context] used with Live Update configuration
* @param liveUpdateConfig the Live Update config object
* @param updateOnAppLoad if a Live Update sync should occur as soon as the Portal loads
* @return the instance of the PortalBuilder with the Live Update config set
* @param context the Android [Context] used with live update configuration.
* @param liveUpdateConfig the live update config object.
* @param updateOnAppLoad if a sync should occur as soon as the Portal loads
* @return the instance of the PortalBuilder with the live update config set.
*/
@JvmOverloads
fun setLiveUpdateConfig(context: Context, liveUpdateConfig: LiveUpdate, updateOnAppLoad: Boolean = true): PortalBuilder {
this.liveUpdateConfig = liveUpdateConfig
this.liveUpdateSource = Portal.LiveUpdateSource.Ionic(liveUpdateConfig)
if(liveUpdateConfig.assetPath == null) {
liveUpdateConfig.assetPath = this._startDir ?: this.name
}
Expand All @@ -561,6 +634,29 @@ class PortalBuilder(val name: String) {
return this
}

/**
* Set a live update provider manager to be used with the Portal.
*
* Example usage (kotlin):
* ```kotlin
* builder = builder.setLiveUpdateProviderManager(providerManager)
* ```
*
* Example usage (java):
* ```java
* builder = builder.setLiveUpdateProviderManager(providerManager);
* ```
*
* @param liveUpdateProviderManager the external live update provider manager. Whether and when it syncs
* on its own (e.g. on construction) is up to the provider implementation; use [Portal.syncProvider]/
* [Portal.syncProviderAsync] to trigger a sync manually.
* @return the instance of the PortalBuilder with the external live update provider manager set.
*/
fun setLiveUpdateProviderManager(liveUpdateProviderManager: ProviderManager): PortalBuilder {
this.liveUpdateSource = Portal.LiveUpdateSource.Provider(liveUpdateProviderManager)
return this
}

/**
* Set development mode on the Portal which will look for a server URL set by the Portals CLI.
* This is set to true by default but can be turned off manually if desired.
Expand Down Expand Up @@ -597,7 +693,7 @@ class PortalBuilder(val name: String) {
portal.addAssetMaps(assetMaps)
portal.initialContext = this.initialContext
portal.portalFragmentType = this.portalFragmentType
portal.liveUpdateConfig = this.liveUpdateConfig
portal.liveUpdateSource = this.liveUpdateSource
portal.devMode = this.devMode
onCreate(portal)
return portal
Expand Down
44 changes: 16 additions & 28 deletions IonicPortals/src/main/kotlin/io/ionic/portals/PortalFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.JavascriptInterface
import androidx.annotation.NonNull
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.getcapacitor.*
import io.ionic.liveupdates.LiveUpdateManager
import org.json.JSONException
import org.json.JSONObject
import java.io.File
Expand Down Expand Up @@ -161,7 +159,7 @@ open class PortalFragment : Fragment {
/**
* Extends the Android Fragment 'onConfigurationChanged' event.
*/
override fun onConfigurationChanged(@NonNull newConfig: Configuration) {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
bridge?.onConfigurationChanged(newConfig)
}
Expand Down Expand Up @@ -265,11 +263,11 @@ open class PortalFragment : Fragment {

/**
* Reloads the Portal.
* If Live Updates is used and the web content was updated, the new content will be loaded.
* If a live update source is configured and the web content was updated, the new content will be loaded.
*/
fun reload() {
if(portal?.liveUpdateConfig != null) {
val latestLiveUpdateFiles = LiveUpdateManager.getLatestAppDirectory(requireContext(), portal?.liveUpdateConfig?.appId!!)
if(portal?.liveUpdateSource != null) {
val latestLiveUpdateFiles = portal?.latestAppDirectory(requireContext())
if (latestLiveUpdateFiles != null) {
if (liveUpdateFiles == null || liveUpdateFiles!!.path != latestLiveUpdateFiles.path) {
liveUpdateFiles = latestLiveUpdateFiles
Expand Down Expand Up @@ -299,7 +297,7 @@ open class PortalFragment : Fragment {
if (existingPortalName != null && portal == null) {
try {
portal = PortalManager.getPortal(existingPortalName)
} catch (e: Exception) {
} catch (_: Exception) {
Logger.warn("Attempted to reload PortalFragment from App restore but portal not found.")
Logger.warn("No portal named $existingPortalName found in PortalManager to use.")
Logger.warn("Portal reload is unsuccessful. This is likely okay and safe to ignore if your app is returning from a force quit state.")
Expand All @@ -323,27 +321,17 @@ open class PortalFragment : Fragment {
.addPluginInstances(initialPluginInstances)
.addWebViewListeners(webViewListeners)

if (portal?.liveUpdateConfig != null) {
liveUpdateFiles = LiveUpdateManager.getLatestAppDirectory(requireContext(), portal?.liveUpdateConfig?.appId!!)
bridgeBuilder = if (liveUpdateFiles != null) {
if (config == null) {
val configFile = File(liveUpdateFiles!!.path + "/capacitor.config.json")
if(configFile.exists()) {
configToUse = CapConfig.loadFromFile(requireContext(), liveUpdateFiles!!.path)
}
}
liveUpdateFiles = portal?.latestAppDirectory(requireContext())

bridgeBuilder.setServerPath(ServerPath(ServerPath.PathType.BASE_PATH, liveUpdateFiles!!.path))
} else {
if (config == null) {
try {
val configFile = requireContext().assets.open("$startDir/capacitor.config.json")
configToUse = CapConfig.loadFromAssets(requireContext(), startDir)
} catch (_: Exception) {}
bridgeBuilder = if (liveUpdateFiles != null) {
if (config == null) {
val configFile = File(liveUpdateFiles!!.path + "/capacitor.config.json")
if(configFile.exists()) {
configToUse = CapConfig.loadFromFile(requireContext(), liveUpdateFiles!!.path)
}

bridgeBuilder.setServerPath(ServerPath(ServerPath.PathType.ASSET_PATH, startDir))
}

bridgeBuilder.setServerPath(ServerPath(ServerPath.PathType.BASE_PATH, liveUpdateFiles!!.path))
} else {
if (config == null) {
try {
Expand All @@ -352,7 +340,7 @@ open class PortalFragment : Fragment {
} catch (_: Exception) {}
}

bridgeBuilder = bridgeBuilder.setServerPath(ServerPath(ServerPath.PathType.ASSET_PATH, startDir))
bridgeBuilder.setServerPath(ServerPath(ServerPath.PathType.ASSET_PATH, startDir))
}

portal?.assetMaps?.let {
Expand Down Expand Up @@ -416,7 +404,7 @@ open class PortalFragment : Fragment {
is String -> {
try {
JSONObject(initialContext)
} catch (ex: JSONException) {
} catch (_: JSONException) {
throw Error("initialContext must be a JSON string or a Map")
}
}
Expand Down Expand Up @@ -484,7 +472,7 @@ open class PortalFragment : Fragment {

when (member.parameters.size) {
1 -> {
val ref = pubSub.subscribe(methodName) { result ->
val ref = pubSub.subscribe(methodName) { _ ->
member.call(messageReceiverParent)
}
subscriptions[methodName] = ref
Expand Down
Loading
Loading