Skip to content

Commit 5643247

Browse files
authored
Merge pull request #20 from addhen/shortCutSupport
Add Shortcut support for launching the viewer app on both Android and iOS
2 parents 209fa98 + 0cac095 commit 5643247

File tree

19 files changed

+245
-21
lines changed

19 files changed

+245
-21
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[![Maven Central](https://img.shields.io/maven-central/v/com.addhen.kanalytics/kanalytics)](https://search.maven.org/search?q=g:com.addhen.kanalytics) ![Build status](https://github.com/addhen/kanalytics/actions/workflows/gradle.yml/badge.svg)
1+
[![Sonatype Snapshots](https://img.shields.io/nexus/s/com.addhen.kanalytics/kanalytics?server=https%3A%2F%2Foss.sonatype.org)](https://oss.sonatype.org/content/repositories/snapshots/com/addhen/kanalytics/) [![Maven Central](https://img.shields.io/maven-central/v/com.addhen.kanalytics/kanalytics)](https://search.maven.org/search?q=g:com.addhen.kanalytics) ![Build status](https://github.com/addhen/kanalytics/actions/workflows/gradle.yml/badge.svg)
22

33

44
# KAnalytics

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ KAnalytics is a flexible analytics library that allows you to:
2626
2. **Interceptor**: Intercept and modify events before they are sent.
2727
3. **Multiplatform Support**: Works on both Android and iOS platforms.
2828
4. **Viewer**: View and debug analytics events during development.
29+
5. **Shortcuts or Quick Actions**: Allows you to launch KAnalytics Viewer via app shortcuts on Android and Quick action on iOS.
2930

3031
## Screenshots
3132

docs/setup.md

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Add `kanalytics` artifact in your project to be able to collect analytics events
1313

1414
```kotlin title="build.gradle.kts" linenums="1"
1515
dependencies {
16-
implementation("com.addhen.kanalytics:kanalytics:1.0.0")
16+
implementation("com.addhen.kanalytics:kanalytics:x.y.z")
1717
}
1818
```
1919

@@ -24,22 +24,22 @@ To view the analytics events, being sent to tracking tools, add the `kanalytics-
2424
```kotlin title="build.gradle.kts" linenums="1"
2525
dependencies {
2626
if (buildType.name == "debug") {
27-
implementation("com.addhen.kanalytics:kanalytics-viewer:1.0.0")
27+
implementation("com.addhen.kanalytics:kanalytics-viewer:x.y.z")
2828
} else {
29-
implementation("com.addhen.kanalytics:kanalytics-viewer-no-op:1.0.0")
29+
implementation("com.addhen.kanalytics:kanalytics-viewer-no-op:x.y.z")
3030
}
3131

3232
//On Android, it will simply be
33-
debugimplementation("com.addhen.kanalytics:kanalytics-viewer:1.0.0")
34-
releaseimplementation("com.addhen.kanalytics:kanalytics-viewer-no-op:1.0.0")
33+
debugimplementation("com.addhen.kanalytics:kanalytics-viewer:x.y.z")
34+
releaseimplementation("com.addhen.kanalytics:kanalytics-viewer-no-op:x.y.z")
3535
}
3636
```
3737

3838
<details>
3939
<summary>Snapshots of the development version are available in Sonatype's snapshots repository.</summary>
4040
<p>
4141

42-
```groovy title="build.gradle.kts" linenums="1"
42+
```kotlin title="build.gradle.kts" linenums="1"
4343
repository {
4444
mavenCentral()
4545
maven {
@@ -48,13 +48,65 @@ repository {
4848
}
4949

5050
dependencies {
51-
implementation("com.addhen.kanalytics:kanalytics:1.0.0-SNAPSHOT")
51+
implementation("com.addhen.kanalytics:kanalytics:x.y.z-SNAPSHOT")
5252
if (buildType.name == "debug") {
53-
implementation("com.addhen.kanalytics:kanalytics-viewer:1.0.0-SNAPSHOT")
53+
implementation("com.addhen.kanalytics:kanalytics-viewer:x.y.z-SNAPSHOT")
5454
} else {
55-
implementation("com.addhen.kanalytics:kanalytics-viewer-no-op:1.0.0-SNAPSHOT")
55+
implementation("com.addhen.kanalytics:kanalytics-viewer-no-op:x.y.z-SNAPSHOT")
5656
}
5757
}
5858
```
5959
</p>
6060
</details>
61+
62+
63+
## Setup Quick Actions on iOS
64+
65+
The quick actions support on iOS requires some manual setup before you can use it.
66+
67+
68+
### Install dependency
69+
70+
Export the `kanalytics-viewer` to used in your swift project:
71+
72+
```kotlin title="ios-framework/build.gradle.kts" linenums="1"
73+
74+
kotlin {
75+
sourceSets {
76+
commonMain {
77+
dependencies {
78+
// Existing dependencies...
79+
// `api` is important here to allow it be exported in the binary framework below
80+
api(projects.kanalyticsViewer)
81+
}
82+
}
83+
84+
targets.withType<KotlinNativeTarget>().configureEach {
85+
binaries.framework {
86+
isStatic = true
87+
baseName = "KAnalyticsViewerKt"
88+
export(projects.kanalyticsViewer)
89+
}
90+
}
91+
}
92+
}
93+
```
94+
95+
### Handle Quick Actions in AppDelegate
96+
Add Quick Actions support by implementing the necessary delegate method in your `AppDelegate` class:
97+
98+
```swift title="AppDelegate.swift" linenums="1"
99+
import KAnalyticsKt
100+
101+
class AppDelegate: NSObject, UIApplicationDelegate {
102+
func application(
103+
_ application: UIApplication,
104+
configurationForConnecting connectingSceneSession: UISceneSession,
105+
options: UIScene.ConnectionOptions
106+
) -> UISceneConfiguration {
107+
return KAnalyticsViewerShorcutHandlerKt.getUISceneConfiguration(configurationForConnectingSceneSession: connectingSceneSession)
108+
}
109+
}
110+
```
111+
112+
For a sample implementation see the sample [iOS app](../sample/ios/iosApp/AppDelegate.swift).

docs/usage.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ If you're using `KAnalyticsViewer` to view the events to be sent and wants to co
8787
// Configure event collection
8888
val collector = KAnalyticsCollector(
8989
showNotification = true,
90+
showShortcut = true,
9091
duration = RetentionPolicyManager.DayDuration(7) // Keep events for 7 days
9192
)
9293

gradle/libs.versions.toml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,16 @@ androidx-core-ktx = "1.15.0"
1616
androidx-activity-compose = "1.10.0"
1717
material = "1.12.0"
1818
appcompat = "1.7.0"
19-
accompanist-permissions = "0.37.0"
20-
navigation-compose = "2.8.0-alpha13" # Make use of type-safe compose navigation
19+
20+
# Version 2.8.0-alpha13 causes this issue on iOS builds:
21+
#
22+
# Uncaught Kotlin exception: kotlin.native.internal.IrLinkageError: Can not read value from
23+
# backing field of property 'androidx_compose_runtime_ProvidedValue$stable': Private backing
24+
# field of property declared in module <org.jetbrains.compose.runtime:runtime> can not be
25+
# accessed in module <org.jetbrains.compose.material3:material3>
26+
# Before you bump this version higher than 2.8.0-alpha13 check the iOS app to make
27+
# it still works.
28+
navigation-compose = "2.8.0-alpha12" # Make use of type-safe compose navigation
2129
kotlinx-serializer = "1.8.0"
2230
lifecycle-viewmodel-compose = "2.8.4"
2331
kermit = "2.0.5"
@@ -37,7 +45,6 @@ espressoCore = "3.6.1"
3745
uiautomator = "2.3.0"
3846
benchmarkMacroJunit4 = "1.3.3"
3947
profileinstaller = "1.4.1"
40-
rules = "1.6.1"
4148

4249
[libraries]
4350
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
@@ -53,7 +60,6 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref =
5360
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
5461
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
5562
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
56-
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist-permissions" }
5763
androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
5864
kotlinx-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serializer" }
5965
lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle-viewmodel-compose" }
@@ -77,7 +83,6 @@ androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-co
7783
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
7884
androidx-benchmark-macro = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" }
7985
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
80-
androidx-rules = { group = "androidx.test", name = "rules", version.ref = "rules" }
8186

8287
[plugins]
8388
android-application = { id = "com.android.application", version.ref = "agp" }

gradle/wrapper/gradle-wrapper.jar

79 Bytes
Binary file not shown.

gradlew

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,7 @@ done
8686
# shellcheck disable=SC2034
8787
APP_BASE_NAME=${0##*/}
8888
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89-
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90-
' "$PWD" ) || exit
89+
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
9190

9291
# Use the maximum available, or set MAX_FD != -1 to use that value.
9392
MAX_FD=maximum

kanalytics-viewer/src/androidMain/kotlin/com/addhen/kanalytics/viewer/LaunchViewerApp.android.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
package com.addhen.kanalytics.viewer
55

66
import android.content.Intent
7+
import androidx.core.content.pm.ShortcutInfoCompat
8+
import androidx.core.content.pm.ShortcutManagerCompat
9+
import androidx.core.graphics.drawable.IconCompat
710
import com.addhen.kanalytics.viewer.app.android.ContextInitializer
811
import com.addhen.kanalytics.viewer.app.android.MainActivity
912

13+
private const val SHORTCUT_ID = "kanalytics_viewer_shortcut"
1014
public actual fun launchViewerApp() {
1115
val context = ContextInitializer.applicationContext
1216
context.startActivity(
@@ -18,3 +22,33 @@ public actual fun launchViewerApp() {
1822
internal actual fun disposeViewerAppWindow() {
1923
MainActivity.viewerAppMainActivityInstance = null
2024
}
25+
26+
internal actual fun setupShortcut(shouldCreateShortcut: Boolean) {
27+
if (shouldCreateShortcut) {
28+
createShortcut()
29+
} else {
30+
ShortcutManagerCompat.removeDynamicShortcuts(
31+
ContextInitializer.applicationContext,
32+
listOf(SHORTCUT_ID),
33+
)
34+
}
35+
}
36+
37+
private fun createShortcut() {
38+
val shortcut = ShortcutInfoCompat.Builder(ContextInitializer.applicationContext, SHORTCUT_ID)
39+
.setShortLabel(ContextInitializer.applicationContext.getText(R.string.viewer_app_name))
40+
.setLongLabel(ContextInitializer.applicationContext.getText(R.string.shortcut_long_label))
41+
.setIcon(
42+
IconCompat.createWithResource(ContextInitializer.applicationContext, R.drawable.ic_app_icon),
43+
)
44+
.setIntent(
45+
Intent(ContextInitializer.applicationContext, MainActivity::class.java)
46+
.also { intent ->
47+
intent.action = Intent.ACTION_VIEW
48+
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
49+
},
50+
)
51+
.build()
52+
53+
ShortcutManagerCompat.pushDynamicShortcut(ContextInitializer.applicationContext, shortcut)
54+
}

kanalytics-viewer/src/androidMain/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
<string name="viewer_app_name">KAnalytics Viewer</string>
44
<string name="notification_title">Analytics Event Sent!</string>
55
<string name="notification_message">%1$s sent to %2$s</string>
6+
<string name="shortcut_long_label">Open KAnalytics Viewer</string>
67
</resources>

kanalytics-viewer/src/commonMain/kotlin/com/addhen/kanalytics/viewer/KAnalyticsCollector.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import kotlinx.datetime.Instant
1616

1717
public class KAnalyticsCollector(
1818
private val showNotification: Boolean = true,
19+
showShortcut: Boolean = true,
1920
duration: RetentionPolicyManager.DayDuration = RetentionPolicyManager.DayDuration(7),
2021
) {
2122
private val scope = MainScope()
@@ -28,6 +29,10 @@ public class KAnalyticsCollector(
2829
private val appCoroutineDispatchers: AppCoroutineDispatchers = AppCoroutineDispatchers()
2930
private val notificationManager: NotificationManager = NotificationManager()
3031

32+
init {
33+
setupShortcut(showShortcut)
34+
}
35+
3136
public fun onEventSent(
3237
kAnalyticsEvent: KAnalyticsEvent,
3338
kTrackerName: KTrackerName,

kanalytics-viewer/src/commonMain/kotlin/com/addhen/kanalytics/viewer/LaunchViewerApp.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ package com.addhen.kanalytics.viewer
66
public expect fun launchViewerApp()
77

88
internal expect fun disposeViewerAppWindow()
9+
10+
internal expect fun setupShortcut(shouldCreateShortcut: Boolean)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2025, Addhen Ltd and the kanalytics project contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package com.addhen.kanalytics.viewer
5+
6+
import kotlinx.cinterop.BetaInteropApi
7+
import platform.UIKit.UIApplicationShortcutItem
8+
import platform.UIKit.UIScene
9+
import platform.UIKit.UISceneConfiguration
10+
import platform.UIKit.UISceneConnectionOptions
11+
import platform.UIKit.UISceneSession
12+
import platform.UIKit.UIWindowScene
13+
import platform.UIKit.UIWindowSceneDelegateProtocol
14+
import platform.darwin.NSObject
15+
16+
// Credits: https://github.com/BVantur/inspektify/blob/main/inspektify/src/iosMain/kotlin/sp/bvantur/inspektify/ktor/client/InspektifyShortcutHandler.kt
17+
internal class KAnalyticsViewerSceneDelegate @OverrideInit constructor() :
18+
NSObject(),
19+
UIWindowSceneDelegateProtocol {
20+
21+
override fun scene(
22+
scene: UIScene,
23+
willConnectToSession: UISceneSession,
24+
options: UISceneConnectionOptions,
25+
) {
26+
onShortcutAction(options.shortcutItem)
27+
}
28+
29+
override fun windowScene(
30+
windowScene: UIWindowScene,
31+
performActionForShortcutItem: UIApplicationShortcutItem,
32+
completionHandler: (Boolean) -> Unit,
33+
) {
34+
onShortcutAction(performActionForShortcutItem)
35+
launchViewerApp()
36+
completionHandler(true)
37+
}
38+
39+
private fun onShortcutAction(shortcutItem: UIApplicationShortcutItem?) {
40+
shortcutItem ?: return
41+
if (shortcutItem.type != getShortcutType()) return
42+
43+
launchViewerApp()
44+
}
45+
}
46+
47+
@OptIn(BetaInteropApi::class)
48+
public fun getUISceneConfiguration(
49+
configurationForConnectingSceneSession: UISceneSession,
50+
): UISceneConfiguration {
51+
val configuration = UISceneConfiguration(
52+
name = configurationForConnectingSceneSession.configuration.name,
53+
sessionRole = configurationForConnectingSceneSession.role,
54+
)
55+
configuration.delegateClass = KAnalyticsViewerSceneDelegate().`class`()
56+
return configuration
57+
}
58+
59+
@Suppress("FunctionOnlyReturningConstant")
60+
public fun getShortcutType(): String = SHORTCUT_TYPE

kanalytics-viewer/src/iosMain/kotlin/com/addhen/kanalytics/viewer/LaunchViewerApp.ios.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ import com.addhen.kanalytics.viewer.app.shared.ui.viewerAppViewController
77
import com.addhen.kanalytics.viewer.app.shared.ui.viewerAppViewControllerInstance
88
import platform.Foundation.NSProcessInfo
99
import platform.UIKit.UIApplication
10+
import platform.UIKit.UIApplicationShortcutIcon
11+
import platform.UIKit.UIApplicationShortcutIconType
12+
import platform.UIKit.UIApplicationShortcutItem
1013
import platform.UIKit.UIModalPresentationFullScreen
1114
import platform.UIKit.UINavigationController
1215
import platform.UIKit.UITabBarController
1316
import platform.UIKit.UIViewController
1417
import platform.UIKit.UIWindow
1518
import platform.UIKit.UIWindowScene
19+
import platform.UIKit.shortcutItems
20+
21+
public const val SHORTCUT_TYPE: String = "com.addhen.kanalytics.viewer.launch"
1622

1723
public actual fun launchViewerApp() {
1824
if (viewerAppViewControllerInstance != null) return // Already launched
@@ -62,3 +68,41 @@ private val topWindow: UIWindow?
6268
.lastOrNull { it.isKeyWindow() }
6369
}
6470
}
71+
72+
internal actual fun setupShortcut(shouldCreateShortcut: Boolean) {
73+
if (shouldCreateShortcut) {
74+
setupShortcut()
75+
} else {
76+
UIApplication.sharedApplication.shortcutItems = UIApplication.sharedApplication
77+
.shortcutItems?.filter {
78+
if (it is UIApplicationShortcutItem) {
79+
it.type != SHORTCUT_TYPE
80+
} else {
81+
true
82+
}
83+
}
84+
}
85+
}
86+
87+
private fun setupShortcut() {
88+
if (UIApplication.sharedApplication.shortcutItems?.any {
89+
(it as? UIApplicationShortcutItem)?.type == SHORTCUT_TYPE
90+
} == true
91+
) {
92+
return
93+
}
94+
val shortcutItem = UIApplicationShortcutItem(
95+
type = SHORTCUT_TYPE,
96+
97+
localizedTitle = "KAnalytics Viewer",
98+
localizedSubtitle = "Open KAnalytics Viewer",
99+
icon = UIApplicationShortcutIcon.iconWithType(
100+
UIApplicationShortcutIconType.UIApplicationShortcutIconTypeCompose,
101+
),
102+
userInfo = mapOf<Any?, Any>(),
103+
)
104+
UIApplication.sharedApplication.shortcutItems =
105+
(UIApplication.sharedApplication.shortcutItems?.toMutableList() ?: mutableListOf()).apply {
106+
add(shortcutItem)
107+
}
108+
}

sample/android/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ dependencies {
5858
implementation(libs.androidx.activity.compose)
5959
implementation(libs.material)
6060
implementation(libs.androidx.appcompat)
61-
implementation(libs.accompanist.permissions)
6261
implementation(libs.androidx.navigation.compose)
6362
implementation(libs.androidx.profileinstaller)
6463
}

0 commit comments

Comments
 (0)