diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 77c2272055..9b162dab29 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -16,7 +16,7 @@ datastore = "1.0.0"
constraintlayout = "2.1.4"
recyclerview = "1.3.0"
lifecycle = "2.5.1"
-activity_compose = "1.6.1"
+activity = "1.6.1"
startup = "1.1.1"
# Compose
@@ -70,7 +70,8 @@ androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process",
androidx_splash = "androidx.core:core-splashscreen:1.0.0"
androidx_security_crypto = "androidx.security:security-crypto:1.0.0"
-androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity_compose" }
+androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" }
+androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
androidx_startup = { module = "androidx.startup:startup-runtime", version.ref = "startup" }
androidx_compose_bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose_bom" }
diff --git a/libraries/androidutils/build.gradle.kts b/libraries/androidutils/build.gradle.kts
index 13da553bbf..8bc0d8e421 100644
--- a/libraries/androidutils/build.gradle.kts
+++ b/libraries/androidutils/build.gradle.kts
@@ -26,5 +26,6 @@ android {
dependencies {
implementation(libs.timber)
implementation(libs.androidx.corektx)
+ implementation(libs.androidx.activity.activity)
implementation(projects.libraries.core)
}
diff --git a/libraries/androidutils/src/main/AndroidManifest.xml b/libraries/androidutils/src/main/AndroidManifest.xml
index 5a19e495ae..8b1ccda517 100644
--- a/libraries/androidutils/src/main/AndroidManifest.xml
+++ b/libraries/androidutils/src/main/AndroidManifest.xml
@@ -17,4 +17,5 @@
+
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/Compat.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/Compat.kt
new file mode 100644
index 0000000000..69761ccbc2
--- /dev/null
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/compat/Compat.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.androidutils.compat
+
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.os.Build
+
+fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): ApplicationInfo {
+ return when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getApplicationInfo(
+ packageName,
+ PackageManager.ApplicationInfoFlags.of(flags.toLong())
+ )
+ else -> @Suppress("DEPRECATION") getApplicationInfo(packageName, flags)
+ }
+}
+
+fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int): PackageInfo {
+ return when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getPackageInfo(
+ packageName,
+ PackageManager.PackageInfoFlags.of(flags.toLong())
+ )
+ else -> @Suppress("DEPRECATION") getPackageInfo(packageName, flags)
+ }
+}
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/intent/PendingIntentCompat.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/intent/PendingIntentCompat.kt
new file mode 100644
index 0000000000..dcdb800a19
--- /dev/null
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/intent/PendingIntentCompat.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.androidutils.intent
+
+import android.app.PendingIntent
+import android.os.Build
+
+object PendingIntentCompat {
+ const val FLAG_IMMUTABLE = PendingIntent.FLAG_IMMUTABLE
+
+ val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_MUTABLE
+ } else {
+ 0
+ }
+}
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/WifiDetector.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/WifiDetector.kt
new file mode 100644
index 0000000000..85b17c6ff8
--- /dev/null
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/WifiDetector.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.androidutils.network
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
+import androidx.core.content.getSystemService
+import io.element.android.libraries.core.bool.orFalse
+import timber.log.Timber
+
+class WifiDetector(
+ context: Context
+) {
+ private val connectivityManager = context.getSystemService()!!
+
+ fun isConnectedToWifi(): Boolean {
+ return connectivityManager.activeNetwork
+ ?.let { connectivityManager.getNetworkCapabilities(it) }
+ ?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
+ .orFalse()
+ .also { Timber.d("isConnected to WiFi: $it") }
+ }
+}
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt
new file mode 100644
index 0000000000..3eaa907303
--- /dev/null
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/CopyToClipboardUseCase.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.androidutils.system
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import androidx.core.content.getSystemService
+
+class CopyToClipboardUseCase(
+ private val context: Context,
+) {
+ fun execute(text: CharSequence) {
+ context.getSystemService()
+ ?.setPrimaryClip(ClipData.newPlainText("", text))
+ }
+}
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt
new file mode 100644
index 0000000000..800da0d5b3
--- /dev/null
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.androidutils.system
+
+import android.annotation.TargetApi
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.PowerManager
+import android.provider.Settings
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.annotation.ChecksSdkIntAtLeast
+import androidx.annotation.RequiresApi
+import androidx.core.content.getSystemService
+import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat
+
+/**
+ * Tells if the application ignores battery optimizations.
+ *
+ * Ignoring them allows the app to run in background to make background sync with the homeserver.
+ * This user option appears on Android M but Android O enforces its usage and kills apps not
+ * authorised by the user to run in background.
+ *
+ * @return true if battery optimisations are ignored
+ */
+fun Context.isIgnoringBatteryOptimizations(): Boolean {
+ // no issue before Android M, battery optimisations did not exist
+ return getSystemService()?.isIgnoringBatteryOptimizations(packageName) == true
+}
+
+fun Context.isAirplaneModeOn(): Boolean {
+ return Settings.Global.getInt(contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) != 0
+}
+
+fun Context.isAnimationEnabled(): Boolean {
+ return Settings.Global.getFloat(contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) != 0f
+}
+
+@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
+fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
+
+/**
+ * Return the application label of the provided package. If not found, the package is returned.
+ */
+fun Context.getApplicationLabel(packageName: String): String {
+ return try {
+ val ai = packageManager.getApplicationInfoCompat(packageName, 0)
+ packageManager.getApplicationLabel(ai).toString()
+ } catch (e: PackageManager.NameNotFoundException) {
+ packageName
+ }
+}
+
+/**
+ * display the system dialog for granting this permission. If previously granted, the
+ * system will not show it (so you should call this method).
+ *
+ * Note: If the user finally does not grant the permission, PushManager.isBackgroundSyncAllowed()
+ * will return false and the notification privacy will fallback to "LOW_DETAIL".
+ */
+fun requestDisablingBatteryOptimization(activity: Activity, activityResultLauncher: ActivityResultLauncher) {
+ val intent = Intent()
+ intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
+ intent.data = Uri.parse("package:" + activity.packageName)
+ activityResultLauncher.launch(intent)
+}
+
+// ==============================================================================================================
+// Clipboard helper
+// ==============================================================================================================
+
+/**
+ * Copy a text to the clipboard, and display a Toast when done.
+ *
+ * @param context the context
+ * @param text the text to copy
+ * @param toastMessage content of the toast message as a String resource. Null for no toast
+ */
+fun copyToClipboard(
+ context: Context,
+ text: CharSequence,
+ toastMessage: String? = null
+) {
+ CopyToClipboardUseCase(context).execute(text)
+ toastMessage?.let { context.toast(it) }
+}
+
+/**
+ * Shows notification settings for the current app.
+ * In android O will directly opens the notification settings, in lower version it will show the App settings
+ */
+fun startNotificationSettingsIntent(context: Context, activityResultLauncher: ActivityResultLauncher) {
+ val intent = Intent()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
+ intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
+ } else {
+ intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
+ intent.putExtra("app_package", context.packageName)
+ intent.putExtra("app_uid", context.applicationInfo?.uid)
+ }
+ activityResultLauncher.launch(intent)
+}
+
+/**
+ * Shows notification system settings for the given channel id.
+ */
+@TargetApi(Build.VERSION_CODES.O)
+fun startNotificationChannelSettingsIntent(activity: Activity, channelID: String) {
+ if (!supportNotificationChannels()) return
+ val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
+ putExtra(Settings.EXTRA_APP_PACKAGE, activity.packageName)
+ putExtra(Settings.EXTRA_CHANNEL_ID, channelID)
+ }
+ activity.startActivity(intent)
+}
+
+fun startAddGoogleAccountIntent(
+ context: Context,
+ activityResultLauncher: ActivityResultLauncher,
+ noActivityFoundMessage: String,
+) {
+ val intent = Intent(Settings.ACTION_ADD_ACCOUNT)
+ intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google"))
+ try {
+ activityResultLauncher.launch(intent)
+ } catch (activityNotFoundException: ActivityNotFoundException) {
+ context.toast(noActivityFoundMessage)
+ }
+}
+
+@RequiresApi(Build.VERSION_CODES.O)
+fun startInstallFromSourceIntent(
+ context: Context,
+ activityResultLauncher: ActivityResultLauncher,
+ noActivityFoundMessage: String,
+) {
+ val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
+ .setData(Uri.parse(String.format("package:%s", context.packageName)))
+ try {
+ activityResultLauncher.launch(intent)
+ } catch (activityNotFoundException: ActivityNotFoundException) {
+ context.toast(noActivityFoundMessage)
+ }
+}
+
+fun startSharePlainTextIntent(
+ context: Context,
+ activityResultLauncher: ActivityResultLauncher?,
+ chooserTitle: String?,
+ text: String,
+ subject: String? = null,
+ extraTitle: String? = null,
+ noActivityFoundMessage: String,
+) {
+ val share = Intent(Intent.ACTION_SEND)
+ share.type = "text/plain"
+ share.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
+ // Add data to the intent, the receiving app will decide what to do with it.
+ share.putExtra(Intent.EXTRA_SUBJECT, subject)
+ share.putExtra(Intent.EXTRA_TEXT, text)
+
+ extraTitle?.let {
+ share.putExtra(Intent.EXTRA_TITLE, it)
+ }
+
+ val intent = Intent.createChooser(share, chooserTitle)
+ try {
+ if (activityResultLauncher != null) {
+ activityResultLauncher.launch(intent)
+ } else {
+ context.startActivity(intent)
+ }
+ } catch (activityNotFoundException: ActivityNotFoundException) {
+ context.toast(noActivityFoundMessage)
+ }
+}
+
+fun startImportTextFromFileIntent(
+ context: Context,
+ activityResultLauncher: ActivityResultLauncher,
+ noActivityFoundMessage: String,
+) {
+ val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
+ type = "text/plain"
+ }
+ try {
+ activityResultLauncher.launch(intent)
+ } catch (activityNotFoundException: ActivityNotFoundException) {
+ context.toast(noActivityFoundMessage)
+ }
+}
+
+// Not in KTX anymore
+fun Context.toast(resId: Int) {
+ Toast.makeText(this, resId, Toast.LENGTH_SHORT).show()
+}
+
+// Not in KTX anymore
+fun Context.toast(message: String) {
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
+}
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottler.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottler.kt
new file mode 100644
index 0000000000..fba6066a64
--- /dev/null
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/throttler/FirstThrottler.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.element.android.libraries.androidutils.throttler
+
+import android.os.SystemClock
+
+/**
+ * Simple ThrottleFirst
+ * See https://raw.githubusercontent.com/wiki/ReactiveX/RxJava/images/rx-operators/throttleFirst.png
+ */
+class FirstThrottler(private val minimumInterval: Long = 800) {
+ private var lastDate = 0L
+
+ sealed class CanHandleResult {
+ object Yes : CanHandleResult()
+ data class No(val shouldWaitMillis: Long) : CanHandleResult()
+
+ fun waitMillis(): Long {
+ return when (this) {
+ Yes -> 0
+ is No -> shouldWaitMillis
+ }
+ }
+ }
+
+ fun canHandle(): CanHandleResult {
+ val now = SystemClock.elapsedRealtime()
+ val delaySinceLast = now - lastDate
+ if (delaySinceLast > minimumInterval) {
+ lastDate = now
+ return CanHandleResult.Yes
+ }
+
+ // Too early
+ return CanHandleResult.No(minimumInterval - delaySinceLast)
+ }
+}
diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/uri/UriExtensions.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/uri/UriExtensions.kt
new file mode 100644
index 0000000000..485a103b5b
--- /dev/null
+++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/uri/UriExtensions.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.androidutils.uri
+
+import android.net.Uri
+
+const val IGNORED_SCHEMA = "ignored"
+
+fun Uri.isIgnored() = scheme == IGNORED_SCHEMA
+
+fun createIgnoredUri(path: String): Uri = Uri.parse("$IGNORED_SCHEMA://$path")
diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt
index 8d041a5f10..52063dd08e 100644
--- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt
+++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt
@@ -65,7 +65,9 @@ fun DependencyHandlerScope.allLibrariesImpl() {
}
fun DependencyHandlerScope.allServicesImpl() {
+ implementation(project(":services:analytics:noop"))
implementation(project(":services:appnavstate:impl"))
+ implementation(project(":services:toolbox:impl"))
}
fun DependencyHandlerScope.allFeaturesApi() {
diff --git a/services/analytics/api/build.gradle.kts b/services/analytics/api/build.gradle.kts
new file mode 100644
index 0000000000..b77829513b
--- /dev/null
+++ b/services/analytics/api/build.gradle.kts
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("io.element.android-library")
+}
+
+android {
+ namespace = "io.element.android.services.analytics.api"
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsTracker.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsTracker.kt
new file mode 100644
index 0000000000..90c2b5cfd4
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsTracker.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api
+
+import io.element.android.services.analytics.api.plan.UserProperties
+
+interface AnalyticsTracker {
+ /**
+ * Capture an Event.
+ */
+ fun capture(event: VectorAnalyticsEvent)
+
+ /**
+ * Track a displayed screen.
+ */
+ fun screen(screen: VectorAnalyticsScreen)
+
+ /**
+ * Update user specific properties.
+ */
+ fun updateUserProperties(userProperties: UserProperties)
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/VectorAnalyticsEvent.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/VectorAnalyticsEvent.kt
new file mode 100644
index 0000000000..49534505c5
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/VectorAnalyticsEvent.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api
+
+interface VectorAnalyticsEvent {
+ fun getName(): String
+ fun getProperties(): Map?
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/VectorAnalyticsScreen.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/VectorAnalyticsScreen.kt
new file mode 100644
index 0000000000..7720158e20
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/VectorAnalyticsScreen.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api
+
+interface VectorAnalyticsScreen {
+ fun getName(): String
+ fun getProperties(): Map?
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/CallEnded.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/CallEnded.kt
new file mode 100644
index 0000000000..63adbaff12
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/CallEnded.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when a call has ended.
+ */
+data class CallEnded(
+ /**
+ * The duration of the call in milliseconds.
+ */
+ val durationMs: Int,
+ /**
+ * Whether its a video call or not.
+ */
+ val isVideo: Boolean,
+ /**
+ * Number of participants in the call.
+ */
+ val numParticipants: Int,
+ /**
+ * Whether this user placed it.
+ */
+ val placed: Boolean,
+) : VectorAnalyticsEvent {
+
+ override fun getName() = "CallEnded"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("durationMs", durationMs)
+ put("isVideo", isVideo)
+ put("numParticipants", numParticipants)
+ put("placed", placed)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/CallError.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/CallError.kt
new file mode 100644
index 0000000000..10225e25c2
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/CallError.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when an error occurred in a call.
+ */
+data class CallError(
+ /**
+ * Whether its a video call or not.
+ */
+ val isVideo: Boolean,
+ /**
+ * Number of participants in the call.
+ */
+ val numParticipants: Int,
+ /**
+ * Whether this user placed it.
+ */
+ val placed: Boolean,
+) : VectorAnalyticsEvent {
+
+ override fun getName() = "CallError"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("isVideo", isVideo)
+ put("numParticipants", numParticipants)
+ put("placed", placed)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/CallStarted.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/CallStarted.kt
new file mode 100644
index 0000000000..1ee9db0d1b
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/CallStarted.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when a call is started.
+ */
+data class CallStarted(
+ /**
+ * Whether its a video call or not.
+ */
+ val isVideo: Boolean,
+ /**
+ * Number of participants in the call.
+ */
+ val numParticipants: Int,
+ /**
+ * Whether this user placed it.
+ */
+ val placed: Boolean,
+) : VectorAnalyticsEvent {
+
+ override fun getName() = "CallStarted"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("isVideo", isVideo)
+ put("numParticipants", numParticipants)
+ put("placed", placed)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/Composer.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/Composer.kt
new file mode 100644
index 0000000000..00a827a166
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/Composer.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when the user sends a message via the composer.
+ */
+data class Composer(
+ /**
+ * Whether the user was using the composer inside of a thread.
+ */
+ val inThread: Boolean,
+ /**
+ * Whether the user's composer interaction was editing a previously sent
+ * event.
+ */
+ val isEditing: Boolean,
+ /**
+ * Whether the user's composer interaction was a reply to a previously
+ * sent event.
+ */
+ val isReply: Boolean,
+ /**
+ * Whether this message begins a new thread or not.
+ */
+ val startsThread: Boolean? = null,
+) : VectorAnalyticsEvent {
+
+ override fun getName() = "Composer"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("inThread", inThread)
+ put("isEditing", isEditing)
+ put("isReply", isReply)
+ startsThread?.let { put("startsThread", it) }
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/CreatedRoom.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/CreatedRoom.kt
new file mode 100644
index 0000000000..1112e732ed
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/CreatedRoom.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when the user creates a room.
+ */
+data class CreatedRoom(
+ /**
+ * Whether the room is a DM.
+ */
+ val isDM: Boolean,
+) : VectorAnalyticsEvent {
+
+ override fun getName() = "CreatedRoom"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("isDM", isDM)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/Error.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/Error.kt
new file mode 100644
index 0000000000..a10fc46ced
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/Error.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when an error occurred.
+ */
+data class Error(
+ /**
+ * Context - client defined, can be used for debugging.
+ */
+ val context: String? = null,
+ /**
+ * Which crypto module is the client currently using.
+ */
+ val cryptoModule: CryptoModule? = null,
+ val domain: Domain,
+ val name: Name,
+) : VectorAnalyticsEvent {
+
+ enum class Domain {
+ E2EE,
+ TO_DEVICE,
+ VOIP,
+ }
+
+ enum class Name {
+ OlmIndexError,
+ OlmKeysNotSentError,
+ OlmUnspecifiedError,
+ ToDeviceFailedToDecrypt,
+ UnknownError,
+ VoipIceFailed,
+ VoipIceTimeout,
+ VoipInviteTimeout,
+ VoipUserHangup,
+ VoipUserMediaFailed,
+ }
+
+ enum class CryptoModule {
+
+ /**
+ * Native / legacy crypto module specific to each platform.
+ */
+ Native,
+
+ /**
+ * Shared / cross-platform crypto module written in Rust.
+ */
+ Rust,
+ }
+
+ override fun getName() = "Error"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ context?.let { put("context", it) }
+ cryptoModule?.let { put("cryptoModule", it.name) }
+ put("domain", domain.name)
+ put("name", name.name)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/Interaction.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/Interaction.kt
new file mode 100644
index 0000000000..f0d0a47d85
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/Interaction.kt
@@ -0,0 +1,468 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when the user clicks/taps/activates a UI element.
+ */
+data class Interaction(
+ /**
+ * The index of the element, if its in a list of elements.
+ */
+ val index: Int? = null,
+ /**
+ * The manner with which the user activated the UI element.
+ */
+ val interactionType: InteractionType? = null,
+ /**
+ * The unique name of this element.
+ */
+ val name: Name,
+) : VectorAnalyticsEvent {
+
+ enum class Name {
+ /**
+ * User tapped the All filter in the All Chats filter tab.
+ */
+ MobileAllChatsFilterAll,
+
+ /**
+ * User tapped the Favourites filter in the All Chats filter tab.
+ */
+ MobileAllChatsFilterFavourites,
+
+ /**
+ * User tapped the People filter in the All Chats filter tab.
+ */
+ MobileAllChatsFilterPeople,
+
+ /**
+ * User tapped the Unreads filter in the All Chats filter tab.
+ */
+ MobileAllChatsFilterUnreads,
+
+ /**
+ * User disabled filters from the all chats layout settings.
+ */
+ MobileAllChatsFiltersDisabled,
+
+ /**
+ * User enabled filters from the all chats layout settings.
+ */
+ MobileAllChatsFiltersEnabled,
+
+ /**
+ * User disabled recents from the all chats layout settings.
+ */
+ MobileAllChatsRecentsDisabled,
+
+ /**
+ * User enabled recents from the all chats layout settings.
+ */
+ MobileAllChatsRecentsEnabled,
+
+ /**
+ * User tapped on Add to Home button on Room Details screen.
+ */
+ MobileRoomAddHome,
+
+ /**
+ * User tapped on Leave Room button on Room Details screen.
+ */
+ MobileRoomLeave,
+
+ /**
+ * User tapped on Threads button on Room screen.
+ */
+ MobileRoomThreadListButton,
+
+ /**
+ * User tapped on a thread summary item on Room screen.
+ */
+ MobileRoomThreadSummaryItem,
+
+ /**
+ * User validated the creation of a new space.
+ */
+ MobileSpaceCreationValidated,
+
+ /**
+ * User tapped on the filter button on ThreadList screen.
+ */
+ MobileThreadListFilterItem,
+
+ /**
+ * User selected a thread on ThreadList screen.
+ */
+ MobileThreadListThreadItem,
+
+ /**
+ * User tapped the already selected space from the space list.
+ */
+ SpacePanelSelectedSpace,
+
+ /**
+ * User tapped an unselected space from the space list -> space
+ * switching should occur.
+ */
+ SpacePanelSwitchSpace,
+
+ /**
+ * User tapped an unselected sub space from the space list -> space
+ * switching should occur.
+ */
+ SpacePanelSwitchSubSpace,
+
+ /**
+ * User clicked the create room button in the add existing room to space
+ * dialog in Element Web/Desktop.
+ */
+ WebAddExistingToSpaceDialogCreateRoomButton,
+
+ /**
+ * User clicked the create DM button in the home page of Element
+ * Web/Desktop.
+ */
+ WebHomeCreateChatButton,
+
+ /**
+ * User clicked the create room button in the home page of Element
+ * Web/Desktop.
+ */
+ WebHomeCreateRoomButton,
+
+ /**
+ * User clicked the explore rooms button in the home page of Element
+ * Web/Desktop.
+ */
+ WebHomeExploreRoomsButton,
+
+ /**
+ * User clicked on the mini avatar uploader in the home page of Element
+ * Web/Desktop.
+ */
+ WebHomeMiniAvatarUploadButton,
+
+ /**
+ * User clicked the explore rooms button next to the search field at the
+ * top of the left panel in Element Web/Desktop.
+ */
+ WebLeftPanelExploreRoomsButton,
+
+ /**
+ * User clicked on the avatar uploader in the profile settings of
+ * Element Web/Desktop.
+ */
+ WebProfileSettingsAvatarUploadButton,
+
+ /**
+ * User interacted with pin to sidebar checkboxes in the quick settings
+ * menu of Element Web/Desktop.
+ */
+ WebQuickSettingsPinToSidebarCheckbox,
+
+ /**
+ * User interacted with the theme dropdown in the quick settings menu of
+ * Element Web/Desktop.
+ */
+ WebQuickSettingsThemeDropdown,
+
+ /**
+ * User accessed the room invite flow using the button at the top of the
+ * room member list in the right panel of Element Web/Desktop.
+ */
+ WebRightPanelMemberListInviteButton,
+
+ /**
+ * User accessed room member list using the 'People' button in the right
+ * panel room summary card of Element Web/Desktop.
+ */
+ WebRightPanelRoomInfoPeopleButton,
+
+ /**
+ * User accessed room settings using the 'Settings' button in the right
+ * panel room summary card of Element Web/Desktop.
+ */
+ WebRightPanelRoomInfoSettingsButton,
+
+ /**
+ * User accessed room member list using the back button in the right
+ * panel user info card of Element Web/Desktop.
+ */
+ WebRightPanelRoomUserInfoBackButton,
+
+ /**
+ * User invited someone to room by clicking invite on the right panel
+ * user info card in Element Web/Desktop.
+ */
+ WebRightPanelRoomUserInfoInviteButton,
+
+ /**
+ * User clicked the threads 'show' filter dropdown in the threads panel
+ * in Element Web/Desktop.
+ */
+ WebRightPanelThreadPanelFilterDropdown,
+
+ /**
+ * User clicked the create room button in the room directory of Element
+ * Web/Desktop.
+ */
+ WebRoomDirectoryCreateRoomButton,
+
+ /**
+ * User clicked the Threads button in the top right of a room in Element
+ * Web/Desktop.
+ */
+ WebRoomHeaderButtonsThreadsButton,
+
+ /**
+ * User adjusted their favourites using the context menu on the header
+ * of a room in Element Web/Desktop.
+ */
+ WebRoomHeaderContextMenuFavouriteToggle,
+
+ /**
+ * User accessed the room invite flow using the context menu on the
+ * header of a room in Element Web/Desktop.
+ */
+ WebRoomHeaderContextMenuInviteItem,
+
+ /**
+ * User interacted with leave action in the context menu on the header
+ * of a room in Element Web/Desktop.
+ */
+ WebRoomHeaderContextMenuLeaveItem,
+
+ /**
+ * User accessed their room notification settings via the context menu
+ * on the header of a room in Element Web/Desktop.
+ */
+ WebRoomHeaderContextMenuNotificationsItem,
+
+ /**
+ * User accessed room member list using the context menu on the header
+ * of a room in Element Web/Desktop.
+ */
+ WebRoomHeaderContextMenuPeopleItem,
+
+ /**
+ * User accessed room settings using the context menu on the header of a
+ * room in Element Web/Desktop.
+ */
+ WebRoomHeaderContextMenuSettingsItem,
+
+ /**
+ * User clicked the create DM button in the + context menu of the room
+ * list header in Element Web/Desktop.
+ */
+ WebRoomListHeaderPlusMenuCreateChatItem,
+
+ /**
+ * User clicked the create room button in the + context menu of the room
+ * list header in Element Web/Desktop.
+ */
+ WebRoomListHeaderPlusMenuCreateRoomItem,
+
+ /**
+ * User clicked the explore rooms button in the + context menu of the
+ * room list header in Element Web/Desktop.
+ */
+ WebRoomListHeaderPlusMenuExploreRoomsItem,
+
+ /**
+ * User adjusted their favourites using the context menu on a room tile
+ * in the room list in Element Web/Desktop.
+ */
+ WebRoomListRoomTileContextMenuFavouriteToggle,
+
+ /**
+ * User accessed the room invite flow using the context menu on a room
+ * tile in the room list in Element Web/Desktop.
+ */
+ WebRoomListRoomTileContextMenuInviteItem,
+
+ /**
+ * User interacted with leave action in the context menu on a room tile
+ * in the room list in Element Web/Desktop.
+ */
+ WebRoomListRoomTileContextMenuLeaveItem,
+
+ /**
+ * User accessed room settings using the context menu on a room tile in
+ * the room list in Element Web/Desktop.
+ */
+ WebRoomListRoomTileContextMenuSettingsItem,
+
+ /**
+ * User accessed their room notification settings via the context menu
+ * on a room tile in the room list in Element Web/Desktop.
+ */
+ WebRoomListRoomTileNotificationsMenu,
+
+ /**
+ * User clicked the create DM button in the + context menu of the rooms
+ * sublist in Element Web/Desktop.
+ */
+ WebRoomListRoomsSublistPlusMenuCreateChatItem,
+
+ /**
+ * User clicked the create room button in the + context menu of the
+ * rooms sublist in Element Web/Desktop.
+ */
+ WebRoomListRoomsSublistPlusMenuCreateRoomItem,
+
+ /**
+ * User clicked the explore rooms button in the + context menu of the
+ * rooms sublist in Element Web/Desktop.
+ */
+ WebRoomListRoomsSublistPlusMenuExploreRoomsItem,
+
+ /**
+ * User clicked on the button to return to the user onboarding list in
+ * the room list in Element Web/Desktop.
+ */
+ WebRoomListUserOnboardingButton,
+
+ /**
+ * User clicked on the button to close the user onboarding button in the
+ * room list in Element Web/Desktop.
+ */
+ WebRoomListUserOnboardingIgnoreButton,
+
+ /**
+ * User interacted with leave action in the general tab of the room
+ * settings dialog in Element Web/Desktop.
+ */
+ WebRoomSettingsLeaveButton,
+
+ /**
+ * User interacted with the prompt to create a new room when adjusting
+ * security settings in an existing room in Element Web/Desktop.
+ */
+ WebRoomSettingsSecurityTabCreateNewRoomButton,
+
+ /**
+ * User clicked a thread summary in the timeline of a room in Element
+ * Web/Desktop.
+ */
+ WebRoomTimelineThreadSummaryButton,
+
+ /**
+ * User interacted with the theme radio selector in the Appearance tab
+ * of Settings in Element Web/Desktop.
+ */
+ WebSettingsAppearanceTabThemeSelector,
+
+ /**
+ * User interacted with the pre-built space checkboxes in the Sidebar
+ * tab of Settings in Element Web/Desktop.
+ */
+ WebSettingsSidebarTabSpacesCheckbox,
+
+ /**
+ * User clicked the explore rooms button in the context menu of a space
+ * in Element Web/Desktop.
+ */
+ WebSpaceContextMenuExploreRoomsItem,
+
+ /**
+ * User clicked the home button in the context menu of a space in
+ * Element Web/Desktop.
+ */
+ WebSpaceContextMenuHomeItem,
+
+ /**
+ * User clicked the new room button in the context menu of a space in
+ * Element Web/Desktop.
+ */
+ WebSpaceContextMenuNewRoomItem,
+
+ /**
+ * User clicked the new room button in the context menu on the space
+ * home in Element Web/Desktop.
+ */
+ WebSpaceHomeCreateRoomButton,
+
+ /**
+ * User clicked the back button on a Thread view going back to the
+ * Threads Panel of Element Web/Desktop.
+ */
+ WebThreadViewBackButton,
+
+ /**
+ * User selected a thread in the Threads panel in Element Web/Desktop.
+ */
+ WebThreadsPanelThreadItem,
+
+ /**
+ * User clicked the theme toggle button in the user menu of Element
+ * Web/Desktop.
+ */
+ WebUserMenuThemeToggleButton,
+
+ /**
+ * User clicked on the send DM CTA in the header of the new user
+ * onboarding page in Element Web/Desktop.
+ */
+ WebUserOnboardingHeaderSendDm,
+
+ /**
+ * User clicked on the action of the download apps task on the new user
+ * onboarding page in Element Web/Desktop.
+ */
+ WebUserOnboardingTaskDownloadApps,
+
+ /**
+ * User clicked on the action of the enable notifications task on the
+ * new user onboarding page in Element Web/Desktop.
+ */
+ WebUserOnboardingTaskEnableNotifications,
+
+ /**
+ * User clicked on the action of the find people task on the new user
+ * onboarding page in Element Web/Desktop.
+ */
+ WebUserOnboardingTaskSendDm,
+
+ /**
+ * User clicked on the action of the your profile task on the new user
+ * onboarding page in Element Web/Desktop.
+ */
+ WebUserOnboardingTaskSetupProfile,
+ }
+
+ enum class InteractionType {
+ Keyboard,
+ Pointer,
+ Touch,
+ }
+
+ override fun getName() = "Interaction"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ index?.let { put("index", it) }
+ interactionType?.let { put("interactionType", it.name) }
+ put("name", name.name)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/JoinedRoom.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/JoinedRoom.kt
new file mode 100644
index 0000000000..d7c86629c7
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/JoinedRoom.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when the user joins a room.
+ */
+data class JoinedRoom(
+ /**
+ * Whether the room is a DM.
+ */
+ val isDM: Boolean,
+ /**
+ * Whether the room is a Space.
+ */
+ val isSpace: Boolean,
+ /**
+ * The size of the room.
+ */
+ val roomSize: RoomSize,
+ /**
+ * The trigger for a room being joined if known.
+ */
+ val trigger: Trigger? = null,
+) : VectorAnalyticsEvent {
+
+ enum class Trigger {
+ /**
+ * Room joined via an invite.
+ */
+ Invite,
+
+ /**
+ * Room joined via link.
+ */
+ MobilePermalink,
+
+ /**
+ * Room joined via a push/desktop notification.
+ */
+ Notification,
+
+ /**
+ * Room joined via the public rooms directory.
+ */
+ RoomDirectory,
+
+ /**
+ * Room joined via its preview.
+ */
+ RoomPreview,
+
+ /**
+ * Room joined via the /join slash command.
+ */
+ SlashCommand,
+
+ /**
+ * Room joined via the space hierarchy view.
+ */
+ SpaceHierarchy,
+
+ /**
+ * Room joined via a timeline pill or link in another room.
+ */
+ Timeline,
+ }
+
+ enum class RoomSize {
+ ElevenToOneHundred,
+ MoreThanAThousand,
+ One,
+ OneHundredAndOneToAThousand,
+ ThreeToTen,
+ Two,
+ }
+
+ override fun getName() = "JoinedRoom"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("isDM", isDM)
+ put("isSpace", isSpace)
+ put("roomSize", roomSize.name)
+ trigger?.let { put("trigger", it.name) }
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/MobileScreen.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/MobileScreen.kt
new file mode 100644
index 0000000000..69e637b01e
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/MobileScreen.kt
@@ -0,0 +1,327 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsScreen
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when the user changed screen on Element Android/iOS.
+ */
+data class MobileScreen(
+ /**
+ * How long the screen was displayed for in milliseconds.
+ */
+ val durationMs: Int? = null,
+ val screenName: ScreenName,
+) : VectorAnalyticsScreen {
+
+ enum class ScreenName {
+ /**
+ * The screen that displays the user's breadcrumbs.
+ */
+ Breadcrumbs,
+
+ /**
+ * The screen shown to create a new (non-direct) room.
+ */
+ CreateRoom,
+
+ /**
+ * The screen shown to create a new space.
+ */
+ CreateSpace,
+
+ /**
+ * The confirmation screen shown before deactivating an account.
+ */
+ DeactivateAccount,
+
+ /**
+ * The tab on mobile that displays the dialpad.
+ */
+ Dialpad,
+
+ /**
+ * The Favourites tab on mobile that lists your favourite people/rooms.
+ */
+ Favourites,
+
+ /**
+ * The form for the forgot password use case.
+ */
+ ForgotPassword,
+
+ /**
+ * Legacy: The screen that shows information about a specific group.
+ */
+ Group,
+
+ /**
+ * The Home tab on iOS | possibly the same on Android?
+ */
+ Home,
+
+ /**
+ * The screen shown to share a link to download the app.
+ */
+ InviteFriends,
+
+ /**
+ * Room accessed via space bottom sheet list.
+ */
+ Invites,
+
+ /**
+ * The screen that displays the login flow (when the user already has an
+ * account).
+ */
+ Login,
+
+ /**
+ * Legacy: The screen that shows all groups/communities you have joined.
+ */
+ MyGroups,
+
+ /**
+ * The People tab on mobile that lists all the DM rooms you have joined.
+ */
+ People,
+
+ /**
+ * The screen that displays the registration flow (when the user wants
+ * to create an account).
+ */
+ Register,
+
+ /**
+ * The screen that displays the messages and events received in a room.
+ */
+ Room,
+
+ /**
+ * The room addresses screen shown from the Room Details screen.
+ */
+ RoomAddresses,
+
+ /**
+ * The screen shown when tapping the name of a room from the Room
+ * screen.
+ */
+ RoomDetails,
+
+ /**
+ * The screen that lists public rooms for you to discover.
+ */
+ RoomDirectory,
+
+ /**
+ * The screen that lists all the user's rooms and let them filter the
+ * rooms.
+ */
+ RoomFilter,
+
+ /**
+ * The screen that displays the list of members that are part of a room.
+ */
+ RoomMembers,
+
+ /**
+ * The notifications settings screen shown from the Room Details screen.
+ */
+ RoomNotifications,
+
+ /**
+ * The roles permissions screen shown from the Room Details screen.
+ */
+ RoomPermissions,
+
+ /**
+ * Screen that displays room preview if user hasn't joined yet.
+ */
+ RoomPreview,
+
+ /**
+ * The screen that allows you to search for messages/files in a specific
+ * room.
+ */
+ RoomSearch,
+
+ /**
+ * The settings screen shown from the Room Details screen.
+ */
+ RoomSettings,
+
+ /**
+ * The screen that allows you to see all of the files sent in a specific
+ * room.
+ */
+ RoomUploads,
+
+ /**
+ * The Rooms tab on mobile that lists all the (non-direct) rooms you've
+ * joined.
+ */
+ Rooms,
+
+ /**
+ * The Files tab shown in the global search screen on Mobile.
+ */
+ SearchFiles,
+
+ /**
+ * The Messages tab shown in the global search screen on Mobile.
+ */
+ SearchMessages,
+
+ /**
+ * The People tab shown in the global search screen on Mobile.
+ */
+ SearchPeople,
+
+ /**
+ * The Rooms tab shown in the global search screen on Mobile.
+ */
+ SearchRooms,
+
+ /**
+ * The global settings screen shown in the app.
+ */
+ Settings,
+
+ /**
+ * The advanced settings screen (developer mode, rageshake, push
+ * notification rules).
+ */
+ SettingsAdvanced,
+
+ /**
+ * The settings screen to change the default notification options.
+ */
+ SettingsDefaultNotifications,
+
+ /**
+ * The settings screen with general profile settings.
+ */
+ SettingsGeneral,
+
+ /**
+ * The Help and About screen.
+ */
+ SettingsHelp,
+
+ /**
+ * The settings screen with list of the ignored users.
+ */
+ SettingsIgnoredUsers,
+
+ /**
+ * The experimental features settings screen.
+ */
+ SettingsLabs,
+
+ /**
+ * The settings screen with legals information.
+ */
+ SettingsLegals,
+
+ /**
+ * The settings screen to manage notification mentions and keywords.
+ */
+ SettingsMentionsAndKeywords,
+
+ /**
+ * The notifications settings screen.
+ */
+ SettingsNotifications,
+
+ /**
+ * The preferences screen (theme, language, editor preferences, etc.
+ */
+ SettingsPreferences,
+
+ /**
+ * The global security settings screen.
+ */
+ SettingsSecurity,
+
+ /**
+ * The calls settings screen.
+ */
+ SettingsVoiceVideo,
+
+ /**
+ * The sidebar shown on mobile with spaces, settings etc.
+ */
+ Sidebar,
+
+ /**
+ * Room accessed via space bottom sheet list.
+ */
+ SpaceBottomSheet,
+
+ /**
+ * Screen that displays the list of rooms and spaces of a space.
+ */
+ SpaceExploreRooms,
+
+ /**
+ * Screen that displays the list of members of a space.
+ */
+ SpaceMembers,
+
+ /**
+ * The bottom sheet that list all space options.
+ */
+ SpaceMenu,
+
+ /**
+ * The screen shown to create a new direct room.
+ */
+ StartChat,
+
+ /**
+ * The screen shown to select which room directory you'd like to use.
+ */
+ SwitchDirectory,
+
+ /**
+ * Screen that displays list of threads for a room.
+ */
+ ThreadList,
+
+ /**
+ * A screen that shows information about a room member.
+ */
+ User,
+
+ /**
+ * The splash screen.
+ */
+ Welcome,
+ }
+
+ override fun getName() = screenName.name
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ durationMs?.let { put("durationMs", it) }
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/PerformanceTimer.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/PerformanceTimer.kt
new file mode 100644
index 0000000000..8296ae783f
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/PerformanceTimer.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered after timing an operation in the app.
+ */
+data class PerformanceTimer(
+ /**
+ * Client defined, can be used for debugging.
+ */
+ val context: String? = null,
+ /**
+ * Client defined, an optional value to indicate how many items were
+ * handled during the operation.
+ */
+ val itemCount: Int? = null,
+ /**
+ * The timer that is being reported.
+ */
+ val name: Name,
+ /**
+ * The time reported by the timer in milliseconds.
+ */
+ val timeMs: Int,
+) : VectorAnalyticsEvent {
+
+ enum class Name {
+ /**
+ * The time spent parsing the response from an initial /sync request. In
+ * this case, `itemCount` should contain the number of joined rooms.
+ */
+ InitialSyncParsing,
+
+ /**
+ * The time spent waiting for a response to an initial /sync request. In
+ * this case, `itemCount` should contain the number of joined rooms.
+ */
+ InitialSyncRequest,
+
+ /**
+ * The time taken to display an event in the timeline that was opened
+ * from a notification.
+ */
+ NotificationsOpenEvent,
+
+ /**
+ * The duration of a regular /sync request when resuming the app. In
+ * this case, `itemCount` should contain the number of joined rooms in
+ * the response.
+ */
+ StartupIncrementalSync,
+
+ /**
+ * The duration of an initial /sync request during startup (if the store
+ * has been wiped). In this case, `itemCount` should contain the number
+ * of joined rooms.
+ */
+ StartupInitialSync,
+
+ /**
+ * How long the app launch screen is displayed for.
+ */
+ StartupLaunchScreen,
+
+ /**
+ * The time to preload data in the MXStore on iOS. In this case,
+ * `itemCount` should contain the number of rooms in the store.
+ */
+ StartupStorePreload,
+
+ /**
+ * The time to load all data from the store (including
+ * StartupStorePreload time). In this case, `itemCount` should contain
+ * the number of rooms loaded into the session
+ */
+ StartupStoreReady,
+ }
+
+ override fun getName() = "PerformanceTimer"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ context?.let { put("context", it) }
+ itemCount?.let { put("itemCount", it) }
+ put("name", name.name)
+ put("timeMs", timeMs)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/PermissionChanged.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/PermissionChanged.kt
new file mode 100644
index 0000000000..9f93078f26
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/PermissionChanged.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when the user changes a permission status.
+ */
+data class PermissionChanged(
+ /**
+ * Whether the permission has been granted by the user.
+ */
+ val granted: Boolean,
+ /**
+ * The name of the permission.
+ */
+ val permission: Permission,
+) : VectorAnalyticsEvent {
+
+ enum class Permission {
+ /**
+ * Permissions related to sending notifications have changed.
+ */
+ Notification,
+ }
+
+ override fun getName() = "PermissionChanged"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("granted", granted)
+ put("permission", permission.name)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/Signup.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/Signup.kt
new file mode 100644
index 0000000000..00b7ef7b60
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/Signup.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered once onboarding has completed, but only if the user registered a
+ * new account.
+ */
+data class Signup(
+ /**
+ * The type of authentication that was used to sign up.
+ */
+ val authenticationType: AuthenticationType,
+) : VectorAnalyticsEvent {
+
+ enum class AuthenticationType {
+ /**
+ * Social login using Apple.
+ */
+ Apple,
+
+ /**
+ * Social login using Facebook.
+ */
+ Facebook,
+
+ /**
+ * Social login using GitHub.
+ */
+ GitHub,
+
+ /**
+ * Social login using GitLab.
+ */
+ GitLab,
+
+ /**
+ * Social login using Google.
+ */
+ Google,
+
+ /**
+ * Registration using some other mechanism such as fallback.
+ */
+ Other,
+
+ /**
+ * Registration with a username and password.
+ */
+ Password,
+
+ /**
+ * Registration using another SSO provider.
+ */
+ SSO,
+ }
+
+ override fun getName() = "Signup"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("authenticationType", authenticationType.name)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/SlashCommand.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/SlashCommand.kt
new file mode 100644
index 0000000000..de0af607b0
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/SlashCommand.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when the user runs a slash command in their composer.
+ */
+data class SlashCommand(
+ /**
+ * The name of this command.
+ */
+ val command: Command,
+) : VectorAnalyticsEvent {
+
+ enum class Command {
+ Invite,
+ Part,
+ }
+
+ override fun getName() = "SlashCommand"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("command", command.name)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/UnauthenticatedError.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/UnauthenticatedError.kt
new file mode 100644
index 0000000000..e235fa994c
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/UnauthenticatedError.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when the user becomes unauthenticated without actually clicking
+ * sign out(E.g. Due to expiry of an access token without a way to refresh).
+ */
+data class UnauthenticatedError(
+ /**
+ * The error code as defined in matrix spec. The source of this error is
+ * from the homeserver.
+ */
+ val errorCode: ErrorCode,
+ /**
+ * The reason for the error. The source of this error is from the
+ * homeserver, the reason can vary and is subject to change so there is
+ * no enum of possible values.
+ */
+ val errorReason: String,
+ /**
+ * Whether the auth mechanism is refresh-token-based.
+ */
+ val refreshTokenAuth: Boolean,
+ /**
+ * Whether a soft logout or hard logout was triggered.
+ */
+ val softLogout: Boolean,
+) : VectorAnalyticsEvent {
+
+ enum class ErrorCode {
+ M_FORBIDDEN,
+ M_UNKNOWN,
+ M_UNKNOWN_TOKEN,
+ }
+
+ override fun getName() = "UnauthenticatedError"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ put("errorCode", errorCode.name)
+ put("errorReason", errorReason)
+ put("refreshTokenAuth", refreshTokenAuth)
+ put("softLogout", softLogout)
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/UserProperties.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/UserProperties.kt
new file mode 100644
index 0000000000..cd72f05af1
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/UserProperties.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * The user properties to apply when identifying. This is not an event
+ * definition. These properties must all be device independent.
+ */
+data class UserProperties(
+ /**
+ * The active filter in the All Chats screen.
+ */
+ val allChatsActiveFilter: AllChatsActiveFilter? = null,
+ /**
+ * The selected messaging use case during the onboarding flow.
+ */
+ val ftueUseCaseSelection: FtueUseCaseSelection? = null,
+ /**
+ * Number of joined rooms the user has favourited.
+ */
+ val numFavouriteRooms: Int? = null,
+ /**
+ * Number of spaces (and sub-spaces) the user is joined to.
+ */
+ val numSpaces: Int? = null,
+) {
+
+ enum class FtueUseCaseSelection {
+ /**
+ * The third option, Communities.
+ */
+ CommunityMessaging,
+
+ /**
+ * The first option, Friends and family.
+ */
+ PersonalMessaging,
+
+ /**
+ * The footer option to skip the question.
+ */
+ Skip,
+
+ /**
+ * The second option, Teams.
+ */
+ WorkMessaging,
+ }
+
+ enum class AllChatsActiveFilter {
+
+ /**
+ * Filters are activated and All is selected.
+ */
+ All,
+
+ /**
+ * Filters are activated and Favourites is selected.
+ */
+ Favourites,
+
+ /**
+ * Filters are activated and People is selected.
+ */
+ People,
+
+ /**
+ * Filters are activated and Unreads is selected.
+ */
+ Unreads,
+ }
+
+ fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ allChatsActiveFilter?.let { put("allChatsActiveFilter", it.name) }
+ ftueUseCaseSelection?.let { put("ftueUseCaseSelection", it.name) }
+ numFavouriteRooms?.let { put("numFavouriteRooms", it) }
+ numSpaces?.let { put("numSpaces", it) }
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/ViewRoom.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/ViewRoom.kt
new file mode 100644
index 0000000000..7477b83b13
--- /dev/null
+++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/plan/ViewRoom.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.api.plan
+
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+
+// GENERATED FILE, DO NOT EDIT. FOR MORE INFORMATION VISIT
+// https://github.com/matrix-org/matrix-analytics-events/
+
+/**
+ * Triggered when the user changes rooms.
+ */
+data class ViewRoom(
+ /**
+ * active space when user navigated to the room.
+ */
+ val activeSpace: ActiveSpace? = null,
+ /**
+ * Whether the room is a DM.
+ */
+ val isDM: Boolean? = null,
+ /**
+ * Whether the room is a Space.
+ */
+ val isSpace: Boolean? = null,
+ /**
+ * The reason for the room change if known.
+ */
+ val trigger: Trigger? = null,
+ /**
+ * Whether the interaction was performed via the keyboard input.
+ */
+ val viaKeyboard: Boolean? = null,
+) : VectorAnalyticsEvent {
+
+ enum class Trigger {
+ /**
+ * Room accessed due to being just created.
+ */
+ Created,
+
+ /**
+ * Room switched due to user interacting with a message search result.
+ */
+ MessageSearch,
+
+ /**
+ * Room switched due to user selecting a user to go to a DM with.
+ */
+ MessageUser,
+
+ /**
+ * Room accessed via space explore.
+ */
+ MobileExploreRooms,
+
+ /**
+ * Room switched due to user interacting with a file search result.
+ */
+ MobileFileSearch,
+
+ /**
+ * Room accessed via interacting with the incall screen.
+ */
+ MobileInCall,
+
+ /**
+ * Room accessed during external sharing.
+ */
+ MobileLinkShare,
+
+ /**
+ * Room accessed via link.
+ */
+ MobilePermalink,
+
+ /**
+ * Room accessed via interacting with direct chat item in the room
+ * contact detail screen.
+ */
+ MobileRoomMemberDetail,
+
+ /**
+ * Room accessed via preview.
+ */
+ MobileRoomPreview,
+
+ /**
+ * Room switched due to user interacting with a room search result.
+ */
+ MobileRoomSearch,
+
+ /**
+ * Room accessed via interacting with direct chat item in the search
+ * contact detail screen.
+ */
+ MobileSearchContactDetail,
+
+ /**
+ * Room accessed via space bottom sheet list.
+ */
+ MobileSpaceBottomSheet,
+
+ /**
+ * Room accessed via interacting with direct chat item in the space
+ * contact detail screen.
+ */
+ MobileSpaceMemberDetail,
+
+ /**
+ * Room accessed via space members list.
+ */
+ MobileSpaceMembers,
+
+ /**
+ * Space accessed via interacting with the space menu.
+ */
+ MobileSpaceMenu,
+
+ /**
+ * Space accessed via interacting with a space settings menu item.
+ */
+ MobileSpaceSettings,
+
+ /**
+ * Room accessed via a push/desktop notification.
+ */
+ Notification,
+
+ /**
+ * Room accessed via the predecessor link at the top of the upgraded
+ * room.
+ */
+ Predecessor,
+
+ /**
+ * Room accessed via the public rooms directory.
+ */
+ RoomDirectory,
+
+ /**
+ * Room accessed via the room list.
+ */
+ RoomList,
+
+ /**
+ * Room accessed via a shortcut.
+ */
+ Shortcut,
+
+ /**
+ * Room accessed via a slash command in Element Web/Desktop like /goto.
+ */
+ SlashCommand,
+
+ /**
+ * Room accessed via the space hierarchy view.
+ */
+ SpaceHierarchy,
+
+ /**
+ * Room accessed via a timeline pill or link in another room.
+ */
+ Timeline,
+
+ /**
+ * Room accessed via a tombstone at the bottom of a predecessor room.
+ */
+ Tombstone,
+
+ /**
+ * Room switched due to user interacting with incoming verification
+ * request.
+ */
+ VerificationRequest,
+
+ /**
+ * Room switched due to accepting a call in a different room in Element
+ * Web/Desktop.
+ */
+ WebAcceptCall,
+
+ /**
+ * Room switched due to making a call via the dial pad in Element
+ * Web/Desktop.
+ */
+ WebDialPad,
+
+ /**
+ * Room accessed via interacting with the floating call or Jitsi PIP in
+ * Element Web/Desktop.
+ */
+ WebFloatingCallWindow,
+
+ /**
+ * Room accessed via the shortcut in Element Web/Desktop's forward
+ * modal.
+ */
+ WebForwardShortcut,
+
+ /**
+ * Room accessed via the Element Web/Desktop horizontal breadcrumbs at
+ * the top of the room list.
+ */
+ WebHorizontalBreadcrumbs,
+
+ /**
+ * Room accessed via an Element Web/Desktop keyboard shortcut like go to
+ * next room with unread messages.
+ */
+ WebKeyboardShortcut,
+
+ /**
+ * Room accessed via Element Web/Desktop's notification panel.
+ */
+ WebNotificationPanel,
+
+ /**
+ * Room accessed via the predecessor link in Settings > Advanced in
+ * Element Web/Desktop.
+ */
+ WebPredecessorSettings,
+
+ /**
+ * Room accessed via clicking on a notifications badge on a room list
+ * sublist in Element Web/Desktop.
+ */
+ WebRoomListNotificationBadge,
+
+ /**
+ * Room switched due to the user changing space in Element Web/Desktop.
+ */
+ WebSpaceContextSwitch,
+
+ /**
+ * Room accessed via clicking on the notifications badge on the
+ * currently selected space in Element Web/Desktop.
+ */
+ WebSpacePanelNotificationBadge,
+
+ /**
+ * Room accessed via Element Web/Desktop's Unified Search modal.
+ */
+ WebUnifiedSearch,
+
+ /**
+ * Room accessed via the Element Web/Desktop vertical breadcrumb hover
+ * menu.
+ */
+ WebVerticalBreadcrumbs,
+
+ /**
+ * Room switched due to widget interaction.
+ */
+ Widget,
+ }
+
+ enum class ActiveSpace {
+
+ /**
+ * Active space is Home.
+ */
+ Home,
+
+ /**
+ * Active space is a meta space.
+ */
+ Meta,
+
+ /**
+ * Active space is a private space.
+ */
+ Private,
+
+ /**
+ * Active space is a public space.
+ */
+ Public,
+ }
+
+ override fun getName() = "ViewRoom"
+
+ override fun getProperties(): Map? {
+ return mutableMapOf().apply {
+ activeSpace?.let { put("activeSpace", it.name) }
+ isDM?.let { put("isDM", it) }
+ isSpace?.let { put("isSpace", it) }
+ trigger?.let { put("trigger", it.name) }
+ viaKeyboard?.let { put("viaKeyboard", it) }
+ }.takeIf { it.isNotEmpty() }
+ }
+}
diff --git a/services/analytics/noop/build.gradle.kts b/services/analytics/noop/build.gradle.kts
new file mode 100644
index 0000000000..fca489c3d3
--- /dev/null
+++ b/services/analytics/noop/build.gradle.kts
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("io.element.android-library")
+ alias(libs.plugins.anvil)
+}
+
+android {
+ namespace = "io.element.android.services.analytics.impl"
+}
+
+anvil {
+ generateDaggerFactories.set(true)
+}
+
+dependencies {
+ implementation(libs.dagger)
+ implementation(projects.libraries.di)
+ api(projects.services.analytics.api)
+}
diff --git a/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/NoopAnalyticsTracker.kt b/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/NoopAnalyticsTracker.kt
new file mode 100644
index 0000000000..1448a06591
--- /dev/null
+++ b/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/NoopAnalyticsTracker.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.analytics.noop
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.services.analytics.api.AnalyticsTracker
+import io.element.android.services.analytics.api.VectorAnalyticsEvent
+import io.element.android.services.analytics.api.VectorAnalyticsScreen
+import io.element.android.services.analytics.api.plan.UserProperties
+import io.element.android.libraries.di.AppScope
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class NoopAnalyticsTracker @Inject constructor() : AnalyticsTracker {
+
+ override fun capture(event: VectorAnalyticsEvent) = Unit
+
+ override fun screen(screen: VectorAnalyticsScreen) = Unit
+
+ override fun updateUserProperties(userProperties: UserProperties) = Unit
+}
diff --git a/services/toolbox/api/build.gradle.kts b/services/toolbox/api/build.gradle.kts
new file mode 100644
index 0000000000..bb748f7ca2
--- /dev/null
+++ b/services/toolbox/api/build.gradle.kts
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("io.element.android-library")
+}
+
+android {
+ namespace = "io.element.android.services.toolbox.api"
+}
+
+dependencies {
+ implementation(libs.androidx.corektx)
+}
diff --git a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/appname/AppNameProvider.kt b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/appname/AppNameProvider.kt
new file mode 100644
index 0000000000..414c9b632e
--- /dev/null
+++ b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/appname/AppNameProvider.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.toolbox.api.appname
+
+interface AppNameProvider {
+ fun getAppName(): String
+}
diff --git a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/strings/StringProvider.kt b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/strings/StringProvider.kt
new file mode 100644
index 0000000000..4233ea3423
--- /dev/null
+++ b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/strings/StringProvider.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.toolbox.api.strings
+
+import androidx.annotation.PluralsRes
+import androidx.annotation.StringRes
+
+interface StringProvider {
+ /**
+ * Returns a localized string from the application's package's
+ * default string table.
+ *
+ * @param resId Resource id for the string
+ * @return The string data associated with the resource, stripped of styled
+ * text information.
+ */
+ fun getString(@StringRes resId: Int): String
+
+ /**
+ * Returns a localized formatted string from the application's package's
+ * default string table, substituting the format arguments as defined in
+ * [java.util.Formatter] and [java.lang.String.format].
+ *
+ * @param resId Resource id for the format string
+ * @param formatArgs The format arguments that will be used for
+ * substitution.
+ * @return The string data associated with the resource, formatted and
+ * stripped of styled text information.
+ */
+ fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String
+ fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String
+}
diff --git a/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/systemclock/SystemClock.kt b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/systemclock/SystemClock.kt
new file mode 100644
index 0000000000..7cde29d7c3
--- /dev/null
+++ b/services/toolbox/api/src/main/kotlin/io/element/android/services/toolbox/api/systemclock/SystemClock.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.toolbox.api.systemclock
+
+interface SystemClock {
+ fun epochMillis(): Long
+}
diff --git a/services/toolbox/impl/build.gradle.kts b/services/toolbox/impl/build.gradle.kts
new file mode 100644
index 0000000000..03cca7957d
--- /dev/null
+++ b/services/toolbox/impl/build.gradle.kts
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id("io.element.android-library")
+ alias(libs.plugins.anvil)
+}
+
+android {
+ namespace = "io.element.android.services.toolbox.impl"
+}
+
+anvil {
+ generateDaggerFactories.set(true)
+}
+
+dependencies {
+ implementation(libs.dagger)
+ implementation(projects.libraries.androidutils)
+ implementation(projects.libraries.di)
+ api(projects.services.toolbox.api)
+ implementation(libs.androidx.corektx)
+}
diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/appname/DefaultAppNameProvider.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/appname/DefaultAppNameProvider.kt
new file mode 100644
index 0000000000..7a5cbd46f0
--- /dev/null
+++ b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/appname/DefaultAppNameProvider.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.toolbox.impl.appname
+
+import android.content.Context
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.androidutils.system.getApplicationLabel
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.ApplicationContext
+import io.element.android.services.toolbox.api.appname.AppNameProvider
+import timber.log.Timber
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultAppNameProvider @Inject constructor(@ApplicationContext private val context: Context) :
+ AppNameProvider {
+
+ override fun getAppName(): String {
+ return try {
+ val appPackageName = context.packageName
+ var appName = context.getApplicationLabel(appPackageName)
+
+ // Use appPackageName instead of appName if appName contains any non-ASCII character
+ if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) {
+ appName = appPackageName
+ }
+ appName
+ } catch (e: Exception) {
+ Timber.e(e, "## AppNameProvider() : failed")
+ "ElementAndroid"
+ }
+ }
+}
diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/strings/AndroidStringProvider.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/strings/AndroidStringProvider.kt
new file mode 100644
index 0000000000..ee3931eabb
--- /dev/null
+++ b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/strings/AndroidStringProvider.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.toolbox.impl.strings
+
+import android.content.res.Resources
+import androidx.annotation.PluralsRes
+import androidx.annotation.StringRes
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
+import io.element.android.services.toolbox.api.strings.StringProvider
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class AndroidStringProvider @Inject constructor(private val resources: Resources) : StringProvider {
+ override fun getString(@StringRes resId: Int): String {
+ return resources.getString(resId)
+ }
+
+ override fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String {
+ return resources.getString(resId, *formatArgs)
+ }
+
+ override fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String {
+ return resources.getQuantityString(resId, quantity, *formatArgs)
+ }
+}
diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/DefaultSystemClock.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/DefaultSystemClock.kt
new file mode 100644
index 0000000000..85479d44b0
--- /dev/null
+++ b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/DefaultSystemClock.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.services.toolbox.impl.systemclock
+
+import com.squareup.anvil.annotations.ContributesBinding
+import io.element.android.libraries.di.AppScope
+import io.element.android.services.toolbox.api.systemclock.SystemClock
+import javax.inject.Inject
+
+@ContributesBinding(AppScope::class)
+class DefaultSystemClock @Inject constructor() : SystemClock {
+
+ /**
+ * Provides a UTC epoch in milliseconds
+ *
+ * This value is not guaranteed to be correct with reality
+ * as a User can override the system time and date to any values.
+ */
+ override fun epochMillis(): Long {
+ return System.currentTimeMillis()
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index e021a267e2..0fd8bc2938 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -66,8 +66,12 @@ include(":libraries:session-storage:api")
include(":libraries:session-storage:impl")
include(":libraries:session-storage:impl-memory")
+include(":services:analytics:api")
+include(":services:analytics:noop")
include(":services:appnavstate:api")
include(":services:appnavstate:impl")
+include(":services:toolbox:api")
+include(":services:toolbox:impl")
include(":features:onboarding:api")
include(":features:onboarding:impl")