Skip to content

Commit

Permalink
feat: android activity routing integration
Browse files Browse the repository at this point in the history
  • Loading branch information
programadorthi committed Feb 2, 2024
1 parent ee41a13 commit b4dc27d
Show file tree
Hide file tree
Showing 19 changed files with 966 additions and 24 deletions.
2 changes: 0 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@ plugins {
alias(libs.plugins.jetbrains.compose) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ktlint) apply false
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
}
2 changes: 2 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ repositories {
dependencies {
implementation(libs.plugin.kotlin)
implementation(libs.plugin.kover)
// FIXME: Kotlin and AGP plugins need to be loaded in the same place
implementation(libs.plugin.android)
}
7 changes: 4 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
plugin-android = "8.2.1"
plugin-android = "8.2.2"
plugin-kover = "0.7.5"
plugin-ktlint = "12.1.0"
plugin-maven = "0.27.0"
Expand All @@ -17,6 +17,7 @@ slf4j = "2.0.4"
voyager = "1.0.0"

junit = "4.13.2"
robolectric = "4.11.1"

# SAMPLE VERSIONS #
core-ktx = "1.9.0"
Expand All @@ -28,6 +29,7 @@ compose-bom = "2022.10.00"
# SAMPLE VERSIONS #

[libraries]
plugin-android = { module = "com.android.tools.build:gradle", version.ref = "plugin-android" }
plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
plugin-kover = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "plugin-kover"}

Expand All @@ -48,6 +50,7 @@ test-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", ve
test-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-debug", version.ref = "coroutines" }
test-junit = { module = "junit:junit", version.ref = "junit" }
test-kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
test-robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }

# SAMPLE LIBRARIES #
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
Expand All @@ -68,8 +71,6 @@ material3 = { group = "androidx.compose.material3", name = "material3" }
# SAMPLE LIBRARIES #

[plugins]
android-application = { id = "com.android.application", version.ref = "plugin-android" }
android-library = { id = "com.android.library", version.ref = "plugin-android" }
jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "plugin-ktlint"}
Expand Down
47 changes: 42 additions & 5 deletions integration/android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,24 +1,61 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
alias(libs.plugins.android.library)
id("com.android.library")
kotlin("android")
id("org.jlleitschuh.gradle.ktlint")
id("org.jetbrains.kotlinx.kover")
alias(libs.plugins.maven.publish)
}

android {
namespace = "dev.programadorthi.routing.android"
compileSdk = 34
namespace = "dev.programadorthi.routing.android"

defaultConfig {
minSdk = 23
consumerProguardFiles("consumer-rules.pro")

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}

tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += listOf("-Xexplicit-api=strict")
}
}

dependencies {
api(projects.resources)
}
implementation(libs.androidx.startup)

testImplementation(kotlin("test"))
testImplementation(libs.test.junit)
testImplementation(libs.test.coroutines)
testImplementation(libs.test.coroutines.debug)
testImplementation(libs.test.kotlin.test.junit)
testImplementation(libs.test.robolectric)
}
Empty file.
Empty file.
18 changes: 17 additions & 1 deletion integration/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,2 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="dev.programadorthi.routing.android.ActivityManager"
android:value="androidx.startup" />
</provider>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package dev.programadorthi.routing.android

import android.app.Activity
import android.app.Application
import android.content.Intent
import android.os.Bundle
import dev.programadorthi.routing.core.RouteMethod
import dev.programadorthi.routing.core.application.ApplicationCall
import dev.programadorthi.routing.core.application.call
import io.ktor.util.pipeline.PipelineContext
import java.lang.ref.WeakReference

@PublishedApi
internal interface ActivityManager : Application.ActivityLifecycleCallbacks {
fun currentActivity(): Activity

fun start(
pipelineContext: PipelineContext<Unit, ApplicationCall>,
intent: Intent,
)
}

internal class AndroidActivityManager : ActivityManager {
private var currentActivity = WeakReference<Activity>(null)

override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?,
) {
}

override fun onActivityResumed(activity: Activity) {}

override fun onActivityPaused(activity: Activity) {}

override fun onActivityStopped(activity: Activity) {}

override fun onActivitySaveInstanceState(
activity: Activity,
outState: Bundle,
) {
}

override fun onActivityDestroyed(activity: Activity) {}

override fun onActivityStarted(activity: Activity) {
currentActivity = WeakReference(activity)
}

override fun currentActivity(): Activity =
currentActivity.get() ?: error(
"Activity manager not started. Please, install AndroidActivities plugin to your route",
)

override fun start(
pipelineContext: PipelineContext<Unit, ApplicationCall>,
intent: Intent,
) = with(pipelineContext) {
val activity = currentActivity()
val options = call.activityOptions
when (val requestCode = call.requestCode) {
null -> activity.startActivity(intent, options)
else -> activity.startActivityForResult(intent, requestCode, options)
}

when (call.routeMethod) {
RouteMethod.Replace -> activity.finishAfterTransition()
RouteMethod.ReplaceAll -> activity.finishAffinity()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package dev.programadorthi.routing.android

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application
import dev.programadorthi.routing.core.application.Application
import dev.programadorthi.routing.core.application.ApplicationCall
import dev.programadorthi.routing.core.application.RouteScopedPlugin
import dev.programadorthi.routing.core.application.createRouteScopedPlugin
import dev.programadorthi.routing.core.application.plugin
import dev.programadorthi.routing.core.application.pluginOrNull
import dev.programadorthi.routing.core.asRouting
import io.ktor.util.AttributeKey
import io.ktor.utils.io.KtorDsl
import android.app.Application as AndroidApplication

internal val AndroidActivityManagerKey =
AttributeKey<ActivityManager>("AndroidActivityManagerKey")

public val AndroidActivities: RouteScopedPlugin<AndroidActivitiesConfig> =
createRouteScopedPlugin("AndroidActivities", ::AndroidActivitiesConfig) {

val context = pluginConfig.context
val parentRouting = application.pluginOrNull(Routing)

var manager =
generateSequence(seed = parentRouting) { it.parent?.asRouting }
.mapNotNull { it.attributes.getOrNull(AndroidActivityManagerKey) }
.firstOrNull()

if (manager == null) {
val applicationContext =
when (context) {
is AndroidApplication -> context
else -> context.applicationContext
}
val application = applicationContext as AndroidApplication
manager = pluginConfig.manager
application.registerActivityLifecycleCallbacks(manager)
}

val activity =
generateSequence(seed = context) { (it as? ContextWrapper)?.baseContext }
.mapNotNull { it as? Activity }
.firstOrNull()

if (activity != null) {
manager.onActivityStarted(activity)
}

application.attributes.put(AndroidActivityManagerKey, manager)

onCall { call ->
call.attributes.put(AndroidActivityManagerKey, manager)
}
}

/**
* A configuration for the [AndroidActivities] plugin.
*/
@KtorDsl
public class AndroidActivitiesConfig {
public lateinit var context: Context

internal var manager: ActivityManager = AndroidActivityManager()
}

@PublishedApi
internal val ApplicationCall.activityManager: ActivityManager
get() = application.activityManager()

internal val Routing.activityManager: ActivityManager
get() = application.activityManager()

private fun Application.activityManager(): ActivityManager {
return when (val manager = attributes.getOrNull(AndroidActivityManagerKey)) {
null -> {
plugin(AndroidActivities)
error("There is no started activity manager. Please, install AndroidActivities plugin")
}

else -> manager
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package dev.programadorthi.routing.android

import android.os.Bundle
import dev.programadorthi.routing.core.RouteMethod
import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application
import dev.programadorthi.routing.core.application.ApplicationCall
import io.ktor.http.Parameters
import io.ktor.util.Attributes

public fun Routing.pushActivity(
name: String = "",
path: String = "",
requestCode: Int? = null,
activityOptions: Bundle? = null,
attributes: Attributes = Attributes(),
parameters: Parameters = Parameters.Empty,
) {
execute(
routeMethod = RouteMethod.Push,
name = name,
path = path,
requestCode = requestCode,
activityOptions = activityOptions,
attributes = attributes,
parameters = parameters,
)
}

public fun Routing.replaceActivity(
name: String = "",
path: String = "",
requestCode: Int? = null,
activityOptions: Bundle? = null,
attributes: Attributes = Attributes(),
parameters: Parameters = Parameters.Empty,
) {
execute(
routeMethod = RouteMethod.Replace,
name = name,
path = path,
requestCode = requestCode,
activityOptions = activityOptions,
attributes = attributes,
parameters = parameters,
)
}

public fun Routing.replaceAllActivity(
name: String = "",
path: String = "",
requestCode: Int? = null,
activityOptions: Bundle? = null,
attributes: Attributes = Attributes(),
parameters: Parameters = Parameters.Empty,
) {
execute(
routeMethod = RouteMethod.ReplaceAll,
name = name,
path = path,
requestCode = requestCode,
activityOptions = activityOptions,
attributes = attributes,
parameters = parameters,
)
}

private fun Routing.execute(
routeMethod: RouteMethod,
name: String = "",
path: String = "",
requestCode: Int?,
activityOptions: Bundle?,
attributes: Attributes = Attributes(),
parameters: Parameters = Parameters.Empty,
) {
val call =
ApplicationCall(
application = application,
name = name,
uri = path,
routeMethod = routeMethod,
attributes = attributes,
parameters = parameters,
)
call.requestCode = requestCode
call.activityOptions = activityOptions
execute(call)
}
Loading

0 comments on commit b4dc27d

Please sign in to comment.