Skip to main content

Android (Jetpack Compose)

Android (Kotlin)

build.gradle.kts

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.play.services)
}

android {
namespace = "com.advantahealth.core.sample"
compileSdk = 36
defaultConfig {
applicationId = "com.advantahealth.core.sample"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.0"
}

buildTypes {
getByName("debug") {
isDebuggable = true
}
getByName("release") {
isMinifyEnabled = false
}
}

buildFeatures {
compose = true
buildConfig = true
}

compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

lint {
warningsAsErrors = false
abortOnError = true
}

kotlin {
jvmToolchain(17)
}
}

dependencies {
implementation("com.advantahealth:core-android:1.1")

coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
debugImplementation("androidx.compose.ui:ui-tooling-preview:1.8.3")

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("androidx.activity:activity-compose:1.10.1")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.compose.ui:ui:1.8.3")
implementation("androidx.compose.ui:ui-tooling:1.8.3")
implementation("androidx.compose.foundation:foundation:1.8.3")
implementation("androidx.compose.material3:material3-android:1.3.2")
implementation("androidx.core:core-ktx:1.16.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.1")
implementation("com.google.android.material:material:1.12.0")
implementation("com.google.firebase:firebase-messaging:25.0.0")
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
>

<activity android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>

<!-- Permission handling for Android 14+ -->
<intent-filter>
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/>
<category android:name="android.intent.category.HEALTH_PERMISSIONS"/>
</intent-filter>
</activity>

</application>

</manifest>

MainActivity.kt

class MainActivity : ComponentActivity() {

private val viewModel: MainViewModel by viewModels()

@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
actionBar?.hide()
FirebaseApp.initializeApp(applicationContext)

AFPermissions.registerLifecycle(this@MainActivity)

setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
AFCoreSampleApp(viewModel)
}
}
}
}

@Composable
fun AFCoreSampleApp(viewModel: MainViewModel) {
if (viewModel.isLoggedIn) {
MainSectionsScreen(this, viewModel)
} else {
LoginScreen(viewModel)
}
}
}

LoginScreen.kt

@Composable
fun LoginScreen(viewModel: MainViewModel) {
val context = LocalContext.current
val forgotPasswordUrl = "https://api.activefitplus.com/recoverpassword"

Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center
) {
Spacer(Modifier.weight(1f))

Text(
text = "Sign in to continue",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)

Spacer(Modifier.height(24.dp))

OutlinedTextField(
value = viewModel.username,
onValueChange = { viewModel.username = it },
label = { Text("Username") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)

Spacer(Modifier.height(16.dp))

OutlinedTextField(
value = viewModel.password,
onValueChange = { viewModel.password = it },
label = { Text("Password") },
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)

Spacer(Modifier.height(8.dp))

Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = {
val intent = Intent(Intent.ACTION_VIEW, forgotPasswordUrl.toUri())
context.startActivity(intent)
}) {
Text(
text = "Forgot Password?",
style = MaterialTheme.typography.labelSmall
)
}
}

Spacer(Modifier.height(8.dp))

Button(
onClick = { viewModel.login() },
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
if (viewModel.isLoading) {
CircularProgressIndicator(
color = Color.White,
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp
)
} else {
Text("Login")
}
}

Spacer(Modifier.weight(1f))
}
}

MainSectionsScreen.kt

@Composable
fun MainSectionsScreen(activity: ComponentActivity, viewModel: MainViewModel) {
val resultText by viewModel.resultTextFlow.collectAsState(initial = "")

Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {

if (resultText.isNotBlank()) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(8.dp)
)
.padding(12.dp)
) {
Text(
text = resultText,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}

Spacer(modifier = Modifier.height(16.dp))
}

Text("🔐 Request Permissions", style = MaterialTheme.typography.titleMedium)

Button(onClick = { viewModel.requestPermission(Permission.FINE_LOCATION) }) {
Text("📍 Location (When In Use)")
}

Button(onClick = { viewModel.requestPermission(Permission.BACKGROUND_LOCATION) }) {
Text("📍 Location (Always)")
}

Button(onClick = { viewModel.requestPermission(Permission.BLUETOOTH_LE) }) {
Text("🔵 Bluetooth LE Access")
}

Button(onClick = { viewModel.requestPermission(Permission.PHYSICAL_ACTIVITY) }) {
Text("🏋️ Motion & Fitness Access")
}

HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)

Text("🔋 Health Connect Actions", style = MaterialTheme.typography.titleMedium)

Button(onClick = { viewModel.connectToHealthConnect(activity) }) {
Text("🔗 Connect to Health Connect")
}

HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)

Text("📍 Geofencing Actions", style = MaterialTheme.typography.titleMedium)

Button(onClick = { viewModel.startGeofencing() }) {
Text("🟢 Start Geofencing")
}

Button(onClick = {
viewModel.stopGeofencing()
}) {
Text("🔴 Stop Geofencing")
}

HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)

Text("📡 API Sample Calls", style = MaterialTheme.typography.titleMedium)

Button(onClick = { viewModel.fetchActivities() }) {
Text("👟 Fetch My Activities")
}

Button(onClick = { viewModel.fetchPayments() }) {
Text("💳 Fetch Payments History")
}

Button(onClick = { viewModel.fetchApplicationSettings() }) {
Text("⚙️ Load Application Settings")
}

Button(onClick = { viewModel.syncMessagingToken(activity) }) {
Text("🔥 Get Firebase Token")
}

Button(onClick = { viewModel.fetchMentalFitnessResources() }) {
Text("🧠 Fetch Mental Fitness Data")
}

Button(onClick = { viewModel.fetchMovementHealthStatus() }) {
Text("🏃‍♂️ Get Movement Health Status")
}

Button(onClick = { viewModel.fetchMyCarePathTasks() }) {
Text("📲 Get MyCarePath URL")
}

Button(onClick = { viewModel.fetchProfileInfo() }) {
Text("👤 Fetch Profile Info")
}

Button(onClick = { viewModel.fetchPrograms() }) {
Text("🎯 Fetch Programs")
}

Button(onClick = { viewModel.fetchSelfReports() }) {
Text("📝 Get Self-Report Data")
}

Button(onClick = { viewModel.fetchSmartWalkingProfile() }) {
Text("🚶 Sync SmartWalking Profile")
}

Button(onClick = { viewModel.syncSmartWalkingSteps() }) {
Text("🚶 Sync SmartWalking Steps")
}

Button(onClick = { viewModel.fetchUserInterfaceItems() }) {
Text("🧩 Load Custom UI Items")
}

Button(onClick = { viewModel.fetchVirtualFitnessClasses() }) {
Text("🎥 Check VirtualFitness (NEOU)")
}

HorizontalDivider(Modifier, DividerDefaults.Thickness, DividerDefaults.color)

Text("🔓 Session", style = MaterialTheme.typography.titleMedium)

Button(onClick = { viewModel.logout() }) {
Text("🚪 Logout")
}

Spacer(modifier = Modifier.height(24.dp))
}
}

MainViewModel.kt

class MainViewModel : ViewModel() {

init {
val config = AFCoreConfig.builder()
.authKey("LcBpmZmekcxtUxPKM4M6k9GzGKvpSZkCoAF1n4g")
.baseUrl("https://arkuspocapi-bismuth.advantahealth.com")
.swAuthKey("LcBpmZmekcxtUxPKM4M6k9GzGKvpSZkCoAF1n4g")
.swBaseUrl("https://qa.smartwalking-bismuth.advantahealth.com")
.enableLogging(true)
.logLevel(AFLogLevel.VERBOSE)
.build()

AFCore.initialize(
config
)
}
var username by mutableStateOf("600009")
var password by mutableStateOf("12345")
var isLoading by mutableStateOf(false)
private val _resultTextFlow = MutableStateFlow("")
val resultTextFlow: StateFlow<String> = _resultTextFlow
var authMessage by mutableStateOf<String?>(null)
var isLoggedIn by mutableStateOf(AFCore.isLoggedIn())

// MARK: - Auth
fun setResult(message: String) {
_resultTextFlow.value = message
}

fun login() {
viewModelScope.launch {
isLoading = true
try {
val loggedIn = AFCore.authentication().login(username, password)
if (loggedIn) {
isLoggedIn = true
authMessage = "Login success"
} else {
authMessage = "Login failed"
}
} catch (e: Exception) {
authMessage = "Login failed: ${e.localizedMessage}"
} finally {
isLoading = false
}
}
}

fun logout() {
viewModelScope.launch {
isLoading = true
try {
AFCore.authentication().logout()
isLoggedIn = false
authMessage = "Logged out"

} catch (e: Exception) {
authMessage = "Logout failed: ${e.localizedMessage}"
} finally {
isLoading = false
}
}
}

// MARK: - Health & Permissions

fun connectToHealthConnect(activity: ComponentActivity) {
launchAndWrap {
val deviceId = Settings.Secure.getString(
activity.contentResolver,
Settings.Secure.ANDROID_ID
)
val granted = AFCore.smartWalking().connectGoogleHealthConnect(activity, deviceId)
setResult("HealthKit permissions $granted")
}
}

fun requestPermission(permission: Permission) {
launchAndWrap {
AFPermissions.requestPermission(permission)
setResult("$permission requested")
}
}

// MARK: - API Actions

fun startGeofencing() {
launchAndWrap {
AFCore.facilities().startMonitoring()
setResult("Geofencing started")
}
}

fun stopGeofencing() {
launchAndWrap {
AFCore.facilities().startMonitoring()
setResult("Geofencing stopped")
}
}

fun fetchActivities() = launchAndWrap {
val result = AFCore.activities().get(month = 7, year = 2025)
setResult("Fetched ${result.activities.size} activities")
}

fun fetchPayments() = launchAndWrap {
val payments = AFCore.payments().get()
setResult("Fetched ${payments.size} payments")
}

fun fetchApplicationSettings() = launchAndWrap {
val settings = AFCore.applicationSettings().getActiveFitSettings()
setResult("Fetched settings and ${settings.urls.count()} Urls")
}

fun syncMessagingToken(activity: ComponentActivity) = launchAndWrap {
val deviceId = Settings.Secure.getString(
activity.contentResolver,
Settings.Secure.ANDROID_ID
)
val token = AFCore.messaging().updateToken(deviceId)
setResult("Firebase Token: $token")
}

fun fetchMentalFitnessResources() = launchAndWrap {
val res = AFCore.mentalFitness().getSpotItTechniqueData()
setResult("Mental Fitness: ${res.situations.size} Situations")
}

fun fetchMovementHealthStatus() = launchAndWrap {
val status = AFCore.movementHealth().isMovrProfileRegistered()
setResult("Movement Health: $status")
}

fun fetchMyCarePathTasks() = launchAndWrap {
val url = AFCore.myCarePath().getMyCarePathAccessUrl(null)
setResult("MyCarePath Access URL: $url")
}

fun fetchProfileInfo() = launchAndWrap {
val profile = AFCore.profile().get()
setResult("Profile: ${profile.firstName}")
}

fun fetchPrograms() = launchAndWrap {
val programs = AFCore.programs().getAvailablePrograms()
setResult("Programs: ${programs.size}")
}

fun fetchSelfReports() = launchAndWrap {
val reports = AFCore.selfReport().get()
setResult("SelfReports: ${reports.selfReportMonths.size}")
}

fun fetchSmartWalkingProfile() = launchAndWrap {
AFCore.smartWalking().getProfile()
setResult("SmartWalking: Synced (check logs)")
}

fun syncSmartWalkingSteps() = launchAndWrap {
AFCore.smartWalking().syncSteps()
setResult("SmartWalking: Synced (check logs)")
}

fun fetchUserInterfaceItems() = launchAndWrap {
val items = AFCore.userInterface().getMenuItems()
setResult("UI Items: ${items.size}")
}

fun fetchVirtualFitnessClasses() = launchAndWrap {
val registered = AFCore.virtualFitness().isMemberRegisteredToNeou()
setResult("Registered to NEOU: $registered")
}

// MARK: - Helper

private fun launchAndWrap(block: suspend () -> Unit) {
viewModelScope.launch {
isLoading = true
try {
block()
} catch (e: Exception) {
setResult("Error: ${e.localizedMessage}")
} finally {
isLoading = false
}
}
}
}