diff --git a/IonicPortals/build.gradle.kts b/IonicPortals/build.gradle.kts index 37c5207..2d9c902 100644 --- a/IonicPortals/build.gradle.kts +++ b/IonicPortals/build.gradle.kts @@ -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") diff --git a/IonicPortals/src/main/kotlin/io/ionic/portals/DevConfiguration.kt b/IonicPortals/src/main/kotlin/io/ionic/portals/DevConfiguration.kt index 354d7f8..ac703c1 100644 --- a/IonicPortals/src/main/kotlin/io/ionic/portals/DevConfiguration.kt +++ b/IonicPortals/src/main/kotlin/io/ionic/portals/DevConfiguration.kt @@ -22,7 +22,7 @@ object DevConfiguration { assetManager.open("$portalDirName/$urlFileName").bufferedReader().use { it.readText() } - } catch (e: Exception) { + } catch (_: Exception) { null } @@ -31,7 +31,7 @@ object DevConfiguration { assetManager.open("$generalDirName/$urlFileName").bufferedReader().use { it.readText() } - } catch (e: Exception) { + } catch (_: Exception) { null } } @@ -50,7 +50,7 @@ object DevConfiguration { var serverConfig = try { val configFile = context.assets.open("$portalDirName/$capConfigFileName") CapConfig.loadFromAssets(context, portalDirName) - } catch (e: Exception) { + } catch (_: Exception) { null } @@ -58,7 +58,7 @@ object DevConfiguration { serverConfig = try { val configFile = context.assets.open("$generalDirName/$capConfigFileName") CapConfig.loadFromAssets(context, generalDirName) - } catch (e: Exception) { + } catch (_: Exception) { null } } diff --git a/IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt b/IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt index f5f7abe..2f59a86 100644 --- a/IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt +++ b/IonicPortals/src/main/kotlin/io/ionic/portals/Portal.kt @@ -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 @@ -65,7 +72,7 @@ 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. @@ -73,22 +80,88 @@ class Portal(val name: String) { 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 = 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. @@ -308,7 +381,7 @@ class PortalBuilder(val name: String) { private var initialContext: Any? = null private var portalFragmentType: Class = 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) { @@ -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 } @@ -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 } @@ -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. @@ -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 diff --git a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalFragment.kt b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalFragment.kt index f539674..a7f4505 100644 --- a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalFragment.kt +++ b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalFragment.kt @@ -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 @@ -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) } @@ -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 @@ -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.") @@ -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 { @@ -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 { @@ -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") } } @@ -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 diff --git a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalManager.kt b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalManager.kt index c296d31..960de33 100644 --- a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalManager.kt +++ b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalManager.kt @@ -53,7 +53,7 @@ object PortalManager { * ``` * * @param name the portal name - * @throws NoSuchElementException throws this exception if the Portal does not exist + * @throws IllegalStateException throws this exception if the Portal does not exist */ @JvmStatic fun getPortal(name: String): Portal { @@ -62,8 +62,7 @@ object PortalManager { /** * Removes the Portal from the Portal Manager. The Portal will be returned if it was present. If not, null is returned. - * Note: if the Portal uses Live Updates and registered an instance on creation, the Live Update instance for the app - * is not removed. + * Note: removing a Portal does not remove its Ionic Live Updates app instance from the Ionic Live Updates manager. * * @param name the name of the Portal to remove */ @@ -92,27 +91,6 @@ object PortalManager { return portals.size } - /** - * Portals registration is no longer required. This function is retained for source - * compatibility and has no effect. - * - * @param key A previously required Portals registration key. - */ - @Deprecated("Portals registration is no longer required. This method has no effect.") - @JvmStatic - fun register(key: String) {} - - /** - * Portals registration is no longer required. - * - * @return true. - */ - @Deprecated("Portals registration is no longer required. This method always returns true.") - @JvmStatic - fun isRegistered(): Boolean { - return true - } - /** * A helper function to build portal classes and add them to the manager. * Classes built with newPortal are added to the PortalManager automatically. diff --git a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalView.kt b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalView.kt index 6121816..0dd3148 100644 --- a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalView.kt +++ b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalView.kt @@ -5,7 +5,6 @@ import android.app.Activity import android.content.Context import android.graphics.Canvas import android.os.Build -import android.os.Handler import android.util.AttributeSet import android.view.View import android.view.WindowInsets @@ -40,7 +39,7 @@ import java.util.ArrayList * * ``` * - * Jetpack Composd example usage: + * Jetpack Compose example usage: * ```kotlin * @Composable * fun loadPortal(portalId: String) { @@ -88,7 +87,7 @@ class PortalView : FrameLayout { this.onBridgeAvailable = onBridgeAvailable this.portalId = portalId this.viewId = viewId - this.id = View.generateViewId() + this.id = generateViewId() loadPortal(context, null) } @@ -100,7 +99,7 @@ class PortalView : FrameLayout { this.portal = portal this.portalId = portal.name this.viewId = viewId - this.id = View.generateViewId() + this.id = generateViewId() loadPortal(context, null) } @@ -264,7 +263,7 @@ class PortalView : FrameLayout { override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean { if (mDrawDisappearingViewsFirst && (mDisappearingFragmentChildren != null - ) && (mDisappearingFragmentChildren!!.size > 0) + ) && (mDisappearingFragmentChildren!!.isNotEmpty()) ) { // If the child is disappearing, we have already drawn it so skip. if (mDisappearingFragmentChildren!!.contains(child)) { @@ -361,4 +360,4 @@ class PortalView : FrameLayout { mDisappearingFragmentChildren!!.add(v) } } -} \ No newline at end of file +} diff --git a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalsPlugin.kt b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalsPlugin.kt index 4559860..122701b 100644 --- a/IonicPortals/src/main/kotlin/io/ionic/portals/PortalsPlugin.kt +++ b/IonicPortals/src/main/kotlin/io/ionic/portals/PortalsPlugin.kt @@ -1,6 +1,5 @@ package io.ionic.portals -import android.util.Log import com.getcapacitor.* import com.getcapacitor.annotation.CapacitorPlugin import org.json.JSONException @@ -65,7 +64,7 @@ class PortalsPubSub { } /** - * A special Capacitor Plugin within the Portals library that allows for bi-directional communication + * A special Capacitor Plugin within the Portals library that allows for bidirectional communication * between Android and web code. It is loaded with every Portal automatically and does not need to be * added like other plugins if the default behavior is desired. * @@ -91,7 +90,7 @@ class PortalsPlugin(private val pubSub: PortalsPubSub = PortalsPubSub.shared) : val data = try { call.data.get("data") - } catch (e: JSONException) { + } catch (_: JSONException) { null } @@ -130,7 +129,7 @@ class PortalsPlugin(private val pubSub: PortalsPubSub = PortalsPubSub.shared) : * @return a map representation of the JSONObject */ fun JSONObject.toMap(): Map { - val map = mutableMapOf(); + val map = mutableMapOf() this.keys().forEach { map[it] = this.get(it) } diff --git a/IonicPortals/src/main/kotlin/io/ionic/portals/WebVitals.kt b/IonicPortals/src/main/kotlin/io/ionic/portals/WebVitals.kt index d96fb7d..7008a49 100644 --- a/IonicPortals/src/main/kotlin/io/ionic/portals/WebVitals.kt +++ b/IonicPortals/src/main/kotlin/io/ionic/portals/WebVitals.kt @@ -4,7 +4,6 @@ import android.webkit.JavascriptInterface import android.webkit.WebView import com.getcapacitor.* import com.getcapacitor.annotation.CapacitorPlugin -import org.json.JSONObject /** * A class providing Web Vitals functionality. When Web Vitals metrics are desired, this class adds