Skip to content

Commit 77b3ea8

Browse files
feat(settings): implement custom API configuration management
- Added functionality to store and retrieve custom API key and base URL in the settings. - Enhanced SDK initialization to support custom configurations, ensuring proper logging and environment handling. - Updated UI components to reflect API configuration status and prompt for app restart after changes. - Introduced methods to clear API settings and validate configuration completeness. - Improved user experience with alerts and streamlined settings management.
1 parent 0a932ed commit 77b3ea8

File tree

24 files changed

+1615
-587
lines changed

24 files changed

+1615
-587
lines changed

examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/RunAnywhereApplication.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.app.Application
44
import android.os.Handler
55
import android.os.Looper
66
import android.util.Log
7+
import com.runanywhere.runanywhereai.presentation.settings.SettingsViewModel
78
import com.runanywhere.sdk.core.onnx.ONNX
89
import com.runanywhere.sdk.core.types.InferenceFramework
910
import com.runanywhere.sdk.llm.llamacpp.LlamaCPP
@@ -105,22 +106,43 @@ class RunAnywhereApplication : Application() {
105106

106107
val startTime = System.currentTimeMillis()
107108

109+
// Check for custom API configuration (stored via Settings screen)
110+
val customApiKey = SettingsViewModel.getStoredApiKey(this@RunAnywhereApplication)
111+
val customBaseURL = SettingsViewModel.getStoredBaseURL(this@RunAnywhereApplication)
112+
val hasCustomConfig = customApiKey != null && customBaseURL != null
113+
114+
if (hasCustomConfig) {
115+
Log.i("RunAnywhereApp", "🔧 Found custom API configuration")
116+
Log.i("RunAnywhereApp", " Base URL: $customBaseURL")
117+
}
118+
108119
// Determine environment based on DEBUG_MODE (NOT BuildConfig.DEBUG!)
109120
// BuildConfig.DEBUG is tied to isDebuggable flag, which we set to true for release builds
110121
// to allow logging. BuildConfig.DEBUG_MODE correctly reflects debug vs release build type.
111-
val environment =
122+
val defaultEnvironment =
112123
if (BuildConfig.DEBUG_MODE) {
113124
SDKEnvironment.DEVELOPMENT
114125
} else {
115126
SDKEnvironment.PRODUCTION
116127
}
117128

129+
// If custom config is set, use production environment to enable the custom backend
130+
val environment = if (hasCustomConfig) SDKEnvironment.PRODUCTION else defaultEnvironment
131+
118132
// Initialize platform context first
119133
AndroidPlatformContext.initialize(this@RunAnywhereApplication)
120134

121135
// Try to initialize SDK - log failures but continue regardless
122136
try {
123-
if (environment == SDKEnvironment.DEVELOPMENT) {
137+
if (hasCustomConfig) {
138+
// Custom configuration mode - use stored API key and base URL
139+
RunAnywhere.initialize(
140+
apiKey = customApiKey!!,
141+
baseURL = customBaseURL!!,
142+
environment = environment,
143+
)
144+
Log.i("RunAnywhereApp", "✅ SDK initialized with CUSTOM configuration (${environment.name.lowercase()})")
145+
} else if (environment == SDKEnvironment.DEVELOPMENT) {
124146
// DEVELOPMENT mode: Don't pass baseURL - SDK uses Supabase URL from C++ dev config
125147
RunAnywhere.initialize(
126148
environment = SDKEnvironment.DEVELOPMENT,

examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsScreen.kt

Lines changed: 211 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import androidx.compose.foundation.clickable
99
import androidx.compose.foundation.layout.*
1010
import androidx.compose.foundation.rememberScrollState
1111
import androidx.compose.foundation.shape.RoundedCornerShape
12+
import androidx.compose.foundation.text.KeyboardOptions
1213
import androidx.compose.foundation.verticalScroll
1314
import androidx.compose.material.icons.Icons
1415
import androidx.compose.material.icons.filled.*
@@ -19,8 +20,10 @@ import androidx.compose.ui.Alignment
1920
import androidx.compose.ui.Modifier
2021
import androidx.compose.ui.graphics.Color
2122
import androidx.compose.ui.platform.LocalContext
22-
import androidx.compose.ui.platform.LocalUriHandler
2323
import androidx.compose.ui.text.font.FontWeight
24+
import androidx.compose.ui.text.input.KeyboardType
25+
import androidx.compose.ui.text.input.PasswordVisualTransformation
26+
import androidx.compose.ui.text.input.VisualTransformation
2427
import androidx.compose.ui.unit.dp
2528
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2629
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -69,6 +72,48 @@ fun SettingsScreen(viewModel: SettingsViewModel = viewModel()) {
6972
)
7073
}
7174

75+
// API Configuration Section
76+
SettingsSection(title = "API Configuration (Testing)") {
77+
ApiConfigurationRow(
78+
label = "API Key",
79+
isConfigured = uiState.isApiKeyConfigured,
80+
)
81+
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
82+
ApiConfigurationRow(
83+
label = "Base URL",
84+
isConfigured = uiState.isBaseURLConfigured,
85+
)
86+
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
87+
Row(
88+
horizontalArrangement = Arrangement.spacedBy(8.dp),
89+
) {
90+
OutlinedButton(
91+
onClick = { viewModel.showApiConfigSheet() },
92+
colors = ButtonDefaults.outlinedButtonColors(
93+
contentColor = AppColors.primaryAccent,
94+
),
95+
) {
96+
Text("Configure")
97+
}
98+
if (uiState.isApiKeyConfigured && uiState.isBaseURLConfigured) {
99+
OutlinedButton(
100+
onClick = { viewModel.clearApiConfiguration() },
101+
colors = ButtonDefaults.outlinedButtonColors(
102+
contentColor = AppColors.primaryRed,
103+
),
104+
) {
105+
Text("Clear")
106+
}
107+
}
108+
}
109+
Spacer(modifier = Modifier.height(8.dp))
110+
Text(
111+
text = "Configure custom API key and base URL for testing. Requires app restart.",
112+
style = MaterialTheme.typography.bodySmall,
113+
color = MaterialTheme.colorScheme.onSurfaceVariant,
114+
)
115+
}
116+
72117
// Storage Overview Section
73118
SettingsSection(
74119
title = "Storage Overview",
@@ -240,6 +285,43 @@ fun SettingsScreen(viewModel: SettingsViewModel = viewModel()) {
240285
},
241286
)
242287
}
288+
289+
// API Configuration Dialog
290+
if (uiState.showApiConfigSheet) {
291+
ApiConfigurationDialog(
292+
apiKey = uiState.apiKey,
293+
baseURL = uiState.baseURL,
294+
onApiKeyChange = { viewModel.updateApiKey(it) },
295+
onBaseURLChange = { viewModel.updateBaseURL(it) },
296+
onSave = { viewModel.saveApiConfiguration() },
297+
onDismiss = { viewModel.hideApiConfigSheet() },
298+
)
299+
}
300+
301+
// Restart Required Dialog
302+
if (uiState.showRestartDialog) {
303+
AlertDialog(
304+
onDismissRequest = { viewModel.dismissRestartDialog() },
305+
title = { Text("Restart Required") },
306+
text = {
307+
Text("API configuration has been updated. Please restart the app for changes to take effect.")
308+
},
309+
confirmButton = {
310+
TextButton(
311+
onClick = { viewModel.dismissRestartDialog() },
312+
) {
313+
Text("OK")
314+
}
315+
},
316+
icon = {
317+
Icon(
318+
imageVector = Icons.Outlined.RestartAlt,
319+
contentDescription = null,
320+
tint = AppColors.primaryOrange,
321+
)
322+
},
323+
)
324+
}
243325
}
244326

245327
/**
@@ -421,3 +503,131 @@ private fun StorageManagementButton(
421503
}
422504
}
423505
}
506+
507+
/**
508+
* API Configuration Row
509+
*/
510+
@Composable
511+
private fun ApiConfigurationRow(
512+
label: String,
513+
isConfigured: Boolean,
514+
) {
515+
Row(
516+
modifier =
517+
Modifier
518+
.fillMaxWidth()
519+
.padding(vertical = 4.dp),
520+
horizontalArrangement = Arrangement.SpaceBetween,
521+
verticalAlignment = Alignment.CenterVertically,
522+
) {
523+
Text(
524+
text = label,
525+
style = MaterialTheme.typography.bodyMedium,
526+
)
527+
Text(
528+
text = if (isConfigured) "Configured" else "Not Set",
529+
style = MaterialTheme.typography.bodySmall,
530+
color = if (isConfigured) AppColors.primaryGreen else AppColors.primaryOrange,
531+
)
532+
}
533+
}
534+
535+
/**
536+
* API Configuration Dialog
537+
*/
538+
@OptIn(ExperimentalMaterial3Api::class)
539+
@Composable
540+
private fun ApiConfigurationDialog(
541+
apiKey: String,
542+
baseURL: String,
543+
onApiKeyChange: (String) -> Unit,
544+
onBaseURLChange: (String) -> Unit,
545+
onSave: () -> Unit,
546+
onDismiss: () -> Unit,
547+
) {
548+
var showPassword by remember { mutableStateOf(false) }
549+
550+
AlertDialog(
551+
onDismissRequest = onDismiss,
552+
title = { Text("API Configuration") },
553+
text = {
554+
Column(
555+
verticalArrangement = Arrangement.spacedBy(16.dp),
556+
) {
557+
// API Key Input
558+
OutlinedTextField(
559+
value = apiKey,
560+
onValueChange = onApiKeyChange,
561+
label = { Text("API Key") },
562+
placeholder = { Text("Enter your API key") },
563+
singleLine = true,
564+
modifier = Modifier.fillMaxWidth(),
565+
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
566+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
567+
trailingIcon = {
568+
IconButton(onClick = { showPassword = !showPassword }) {
569+
Icon(
570+
imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
571+
contentDescription = if (showPassword) "Hide password" else "Show password",
572+
)
573+
}
574+
},
575+
supportingText = {
576+
Text("Your API key for authenticating with the backend")
577+
},
578+
)
579+
580+
// Base URL Input
581+
OutlinedTextField(
582+
value = baseURL,
583+
onValueChange = onBaseURLChange,
584+
label = { Text("Base URL") },
585+
placeholder = { Text("https://api.example.com") },
586+
singleLine = true,
587+
modifier = Modifier.fillMaxWidth(),
588+
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
589+
supportingText = {
590+
Text("The backend API URL (https:// added automatically if missing)")
591+
},
592+
)
593+
594+
// Warning
595+
Surface(
596+
color = AppColors.primaryOrange.copy(alpha = 0.1f),
597+
shape = RoundedCornerShape(8.dp),
598+
) {
599+
Row(
600+
modifier = Modifier.padding(12.dp),
601+
horizontalArrangement = Arrangement.spacedBy(8.dp),
602+
verticalAlignment = Alignment.Top,
603+
) {
604+
Icon(
605+
imageVector = Icons.Outlined.Warning,
606+
contentDescription = null,
607+
tint = AppColors.primaryOrange,
608+
modifier = Modifier.size(20.dp),
609+
)
610+
Text(
611+
text = "After saving, you must restart the app for changes to take effect. The SDK will reinitialize with your custom configuration.",
612+
style = MaterialTheme.typography.bodySmall,
613+
color = MaterialTheme.colorScheme.onSurfaceVariant,
614+
)
615+
}
616+
}
617+
}
618+
},
619+
confirmButton = {
620+
TextButton(
621+
onClick = onSave,
622+
enabled = apiKey.isNotEmpty() && baseURL.isNotEmpty(),
623+
) {
624+
Text("Save")
625+
}
626+
},
627+
dismissButton = {
628+
TextButton(onClick = onDismiss) {
629+
Text("Cancel")
630+
}
631+
},
632+
)
633+
}

0 commit comments

Comments
 (0)