From 4ac3114064e82aa3b0f6360fc2872442a39dc932 Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Sat, 27 Dec 2025 19:43:49 -0500 Subject: [PATCH] Add integrated onboarding carousel with permissions and server setup Replaces separate welcome and server setup screens with a unified 5-page onboarding carousel that guides users through: - App introduction and backend requirements - Notification permission with explanation - All files access permission with explanation - Server configuration (address, port, key) with QR scanning - Completion confirmation Also adds notification permission checks before uploads to ensure upload progress notifications can be displayed. Users are prompted for permission if not already granted, then upload proceeds automatically. Changes: - Create OnboardingScreen.kt with 5-page carousel flow - Add onboarding completion tracking to PreferencesUtil - Update navigation to show onboarding on first launch - Add notification permission checks to upload tabs - Add roundIcon attribute to manifest for better icon display --- app/src/main/AndroidManifest.xml | 1 + .../simplelogupload/ComposeMainActivity.kt | 7 + .../navigation/AppNavigation.kt | 13 + .../simplelogupload/navigation/Screen.kt | 5 + .../ui/screens/OnboardingScreen.kt | 601 ++++++++++++++++++ .../ui/screens/tabs/DumpstateTab.kt | 45 +- .../ui/screens/tabs/GeneralUploadTab.kt | 49 +- .../simplelogupload/util/PreferencesUtil.java | 27 +- 8 files changed, 738 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/mattintech/simplelogupload/ui/screens/OnboardingScreen.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8796be1..df19b41 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ Unit +) { + val context = LocalContext.current + val activity = context as Activity + val pagerState = rememberPagerState(pageCount = { 5 }) + val scope = rememberCoroutineScope() + + // Permission states + var notificationPermissionGranted by remember { + mutableStateOf(PermissionUtil.hasNotificationPermission(context)) + } + var filesPermissionGranted by remember { + mutableStateOf(PermissionUtil.hasAllFilesAccessPermission()) + } + + // Server configuration states + var serverAddress by remember { mutableStateOf("") } + var serverPort by remember { mutableStateOf("") } + var clientKey by remember { mutableStateOf("") } + var serverAddressError by remember { mutableStateOf(null) } + var serverPortError by remember { mutableStateOf(null) } + var clientKeyError by remember { mutableStateOf(null) } + + // Notification permission launcher + val notificationPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { isGranted -> + notificationPermissionGranted = isGranted + if (isGranted) { + // Auto-advance to next page after permission granted + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } else { + Toast.makeText( + context, + "Notification permission helps you track upload progress", + Toast.LENGTH_SHORT + ).show() + // Still allow advancing even if denied + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } + } + + // Files access launcher + val filesPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { + filesPermissionGranted = PermissionUtil.hasAllFilesAccessPermission() + if (filesPermissionGranted) { + // Auto-advance to server setup page + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } else { + Toast.makeText( + context, + "File access is needed to upload files from your device", + Toast.LENGTH_SHORT + ).show() + // Still allow advancing even if denied + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } + } + + // QR scanner launcher + val qrScannerLauncher = rememberLauncherForActivityResult( + contract = ScanContract() + ) { result -> + QrScannerUtil.processScanResult( + result.contents, + object : QrScannerUtil.QrScanCallback { + override fun onQrScanned(config: QrServerConfig) { + serverAddress = config.server + serverPort = config.port.toString() + clientKey = config.key + Toast.makeText( + context, + R.string.qr_scan_success, + Toast.LENGTH_SHORT + ).show() + } + + override fun onQrScanCancelled() { + Toast.makeText( + context, + R.string.qr_scan_cancelled, + Toast.LENGTH_SHORT + ).show() + } + + override fun onQrScanError(errorMessage: String) { + Toast.makeText( + context, + context.getString(R.string.qr_scan_error, errorMessage), + Toast.LENGTH_LONG + ).show() + } + } + ) + } + + // Camera permission launcher (for QR scanning) + val cameraPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + val options = QrScannerUtil.createScanOptions() + qrScannerLauncher.launch(options) + } else { + Toast.makeText( + context, + R.string.camera_permission_denied, + Toast.LENGTH_LONG + ).show() + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + // Pager + HorizontalPager( + state = pagerState, + modifier = Modifier.weight(1f), + userScrollEnabled = false // Disable manual scrolling + ) { page -> + when (page) { + 0 -> WelcomePage( + onContinue = { + scope.launch { + pagerState.animateScrollToPage(1) + } + } + ) + 1 -> NotificationPermissionPage( + isGranted = notificationPermissionGranted, + onRequestPermission = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + // On older versions, permission is granted by default + notificationPermissionGranted = true + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } + } + ) + 2 -> FilesPermissionPage( + isGranted = filesPermissionGranted, + onRequestPermission = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + intent.data = Uri.parse("package:" + context.packageName) + filesPermissionLauncher.launch(intent) + } catch (e: Exception) { + val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) + filesPermissionLauncher.launch(intent) + } + } else { + // On older versions, permission is granted by default + filesPermissionGranted = true + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } + } + ) + 3 -> ServerSetupPage( + serverAddress = serverAddress, + serverPort = serverPort, + clientKey = clientKey, + serverAddressError = serverAddressError, + serverPortError = serverPortError, + clientKeyError = clientKeyError, + onServerAddressChange = { + serverAddress = it + serverAddressError = null + }, + onServerPortChange = { + serverPort = it + serverPortError = null + }, + onClientKeyChange = { + clientKey = it + clientKeyError = null + }, + onScanQr = { + cameraPermissionLauncher.launch(Manifest.permission.CAMERA) + }, + onContinue = { + // Validate server settings + var hasError = false + + if (serverAddress.isBlank()) { + serverAddressError = "Server address is required" + hasError = true + } + + if (serverPort.isBlank()) { + serverPortError = "Server port is required" + hasError = true + } else { + val port = serverPort.toIntOrNull() + if (port == null || port <= 0 || port > 65535) { + serverPortError = "Port must be between 1 and 65535" + hasError = true + } + } + + if (clientKey.isBlank()) { + clientKeyError = "Client key is required" + hasError = true + } + + if (!hasError) { + // Save settings + PreferencesUtil.setServerAddress(context, serverAddress) + PreferencesUtil.setServerPort(context, serverPort.toInt()) + PreferencesUtil.setClientKey(context, clientKey) + PreferencesUtil.setSetupCompleted(context, true) + + // Advance to final page + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } + } + ) + 4 -> ReadyPage( + onGetStarted = { + // Mark onboarding as complete + PreferencesUtil.setOnboardingCompleted(context, true) + onOnboardingComplete() + } + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Page indicator + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + repeat(5) { iteration -> + val color = if (pagerState.currentPage == iteration) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) + } + Box( + modifier = Modifier + .padding(4.dp) + .size(8.dp) + .background(color, shape = MaterialTheme.shapes.small) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +/** + * Welcome page explaining the app + */ +@Composable +private fun WelcomePage( + onContinue: () -> Unit +) { + OnboardingPageTemplate( + icon = Icons.Default.CloudUpload, + title = "Welcome to SimpleLogUpload", + description = "A simple and secure way to upload files to your server.\n\n" + + "This app requires a backend server to function. Your administrator should have " + + "provided you with server details.\n\n" + + "Let's get your app set up!", + buttonText = "Continue", + onButtonClick = onContinue + ) +} + +/** + * Notification permission page + */ +@Composable +private fun NotificationPermissionPage( + isGranted: Boolean, + onRequestPermission: () -> Unit +) { + OnboardingPageTemplate( + icon = Icons.Default.Notifications, + title = "Upload Progress Notifications", + description = "SimpleLogUpload needs notification permission to:\n\n" + + "• Show upload progress in real-time\n" + + "• Display upload completion status\n" + + "• Keep you informed about file transfers\n\n" + + (if (isGranted) "✓ Permission granted!" else "This helps you track uploads even when the app is in the background."), + buttonText = if (isGranted) "Continue" else "Grant Permission", + onButtonClick = onRequestPermission, + isPermissionGranted = isGranted + ) +} + +/** + * Files permission page + */ +@Composable +private fun FilesPermissionPage( + isGranted: Boolean, + onRequestPermission: () -> Unit +) { + OnboardingPageTemplate( + icon = Icons.Default.Folder, + title = "File Access Permission", + description = "SimpleLogUpload needs access to your files to:\n\n" + + "• Browse and select files to upload\n" + + "• Access dumpstate files (on Samsung devices)\n" + + "• Read files from any location on your device\n\n" + + (if (isGranted) "✓ Permission granted!" else "You'll be taken to Settings to grant this permission."), + buttonText = if (isGranted) "Continue" else "Grant Permission", + onButtonClick = onRequestPermission, + isPermissionGranted = isGranted + ) +} + +/** + * Server setup page + */ +@Composable +private fun ServerSetupPage( + serverAddress: String, + serverPort: String, + clientKey: String, + serverAddressError: String?, + serverPortError: String?, + clientKeyError: String?, + onServerAddressChange: (String) -> Unit, + onServerPortChange: (String) -> Unit, + onClientKeyChange: (String) -> Unit, + onScanQr: () -> Unit, + onContinue: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Top + ) { + Spacer(modifier = Modifier.height(8.dp)) + + Icon( + imageVector = Icons.Default.Storage, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "Server Configuration", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Enter your server details or scan a QR code from your admin panel.", + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(24.dp)) + + // Server Address + OutlinedTextField( + value = serverAddress, + onValueChange = onServerAddressChange, + label = { Text("Server Address") }, + placeholder = { Text("e.g., 192.168.1.100") }, + modifier = Modifier.fillMaxWidth(), + isError = serverAddressError != null, + supportingText = serverAddressError?.let { { Text(it) } }, + singleLine = true + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // Server Port + OutlinedTextField( + value = serverPort, + onValueChange = onServerPortChange, + label = { Text("Server Port") }, + placeholder = { Text("e.g., 8080") }, + modifier = Modifier.fillMaxWidth(), + isError = serverPortError != null, + supportingText = serverPortError?.let { { Text(it) } }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // Client Key + OutlinedTextField( + value = clientKey, + onValueChange = onClientKeyChange, + label = { Text("Client Key") }, + placeholder = { Text("Authentication key") }, + modifier = Modifier.fillMaxWidth(), + isError = clientKeyError != null, + supportingText = clientKeyError?.let { { Text(it) } }, + singleLine = true + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // QR Scan Button + OutlinedButton( + onClick = onScanQr, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + Icons.Default.QrCodeScanner, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Scan QR Code") + } + } + + Button( + onClick = onContinue, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) { + Text( + text = "Continue", + modifier = Modifier.padding(vertical = 8.dp) + ) + } + } +} + +/** + * Ready page - final page + */ +@Composable +private fun ReadyPage( + onGetStarted: () -> Unit +) { + OnboardingPageTemplate( + icon = Icons.Default.Check, + title = "You're All Set!", + description = "Your app is now configured and ready to use!\n\n" + + "✓ Permissions granted\n" + + "✓ Server configured\n\n" + + "You can now upload files to your server. " + + "Tap the button below to start using SimpleLogUpload!", + buttonText = "Start Using App", + onButtonClick = onGetStarted + ) +} + +/** + * Reusable template for onboarding pages + */ +@Composable +private fun OnboardingPageTemplate( + icon: androidx.compose.ui.graphics.vector.ImageVector, + title: String, + description: String, + buttonText: String, + onButtonClick: () -> Unit, + isPermissionGranted: Boolean = false +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(80.dp), + tint = MaterialTheme.colorScheme.primary + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = title, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = description, + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Button( + onClick = onButtonClick, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) { + Text( + text = buttonText, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + } +} diff --git a/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/DumpstateTab.kt b/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/DumpstateTab.kt index 97b5478..34707a8 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/DumpstateTab.kt +++ b/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/DumpstateTab.kt @@ -1,6 +1,10 @@ package com.mattintech.simplelogupload.ui.screens.tabs +import android.Manifest +import android.os.Build import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -13,6 +17,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.mattintech.simplelogupload.ui.components.UploadParamsDialog import com.mattintech.simplelogupload.ui.components.UploadResultDialog +import com.mattintech.simplelogupload.util.PermissionUtil import com.mattintech.simplelogupload.util.PreferencesUtil import com.mattintech.simplelogupload.viewmodel.MainViewModel import java.text.SimpleDateFormat @@ -33,6 +38,42 @@ fun DumpstateTab( val context = LocalContext.current val uiState by viewModel.uiState.collectAsState() + // State to track pending upload parameters + var pendingUpload by remember { mutableStateOf?>(null) } + + // Notification permission launcher + val notificationPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + // Permission granted, proceed with pending upload + pendingUpload?.let { (maxDownloads, expiryHours) -> + viewModel.uploadSelectedDumpstates(maxDownloads, expiryHours) + } + pendingUpload = null + } else { + Toast.makeText( + context, + "Notification permission is required to show upload progress", + Toast.LENGTH_LONG + ).show() + } + } + + // Helper function to check permission and upload + val uploadWithPermissionCheck: (Int, Int) -> Unit = { maxDownloads, expiryHours -> + if (PermissionUtil.hasNotificationPermission(context)) { + // Permission already granted, proceed with upload + viewModel.uploadSelectedDumpstates(maxDownloads, expiryHours) + } else { + // Need to request permission first + pendingUpload = Pair(maxDownloads, expiryHours) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + } + // Show error toast LaunchedEffect(uiState.errorMessage) { uiState.errorMessage?.let { error -> @@ -145,7 +186,7 @@ fun DumpstateTab( } else { val maxDownloads = PreferencesUtil.getDefaultMaxDownloads(context) val expiryHours = PreferencesUtil.getDefaultExpiryHours(context) - viewModel.uploadSelectedDumpstates(maxDownloads, expiryHours) + uploadWithPermissionCheck(maxDownloads, expiryHours) } }, modifier = Modifier.fillMaxWidth(), @@ -177,7 +218,7 @@ fun DumpstateTab( UploadParamsDialog( onDismiss = { viewModel.hideUploadParamsDialog() }, onConfirm = { maxDownloads, expiryHours -> - viewModel.uploadSelectedDumpstates(maxDownloads, expiryHours) + uploadWithPermissionCheck(maxDownloads, expiryHours) } ) } diff --git a/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/GeneralUploadTab.kt b/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/GeneralUploadTab.kt index 49d05cf..9d28f1e 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/GeneralUploadTab.kt +++ b/app/src/main/java/com/mattintech/simplelogupload/ui/screens/tabs/GeneralUploadTab.kt @@ -1,5 +1,6 @@ package com.mattintech.simplelogupload.ui.screens.tabs +import android.Manifest import android.app.Activity import android.content.Intent import android.net.Uri @@ -40,6 +41,30 @@ fun GeneralUploadTab( val activity = context as Activity val uiState by viewModel.uiState.collectAsState() + // State to track pending upload parameters + var pendingUpload by remember { mutableStateOf?>(null) } + + // Notification permission launcher + val notificationPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + // Permission granted, proceed with pending upload + pendingUpload?.let { (maxDownloads, expiryHours) -> + uiState.selectedFile?.let { file -> + viewModel.uploadFile(file, maxDownloads, expiryHours) + } + } + pendingUpload = null + } else { + Toast.makeText( + context, + "Notification permission is required to show upload progress", + Toast.LENGTH_LONG + ).show() + } + } + // File picker launcher val filePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() @@ -65,6 +90,22 @@ fun GeneralUploadTab( } } + // Helper function to check permission and upload + val uploadWithPermissionCheck: (Int, Int) -> Unit = { maxDownloads, expiryHours -> + if (PermissionUtil.hasNotificationPermission(context)) { + // Permission already granted, proceed with upload + uiState.selectedFile?.let { file -> + viewModel.uploadFile(file, maxDownloads, expiryHours) + } + } else { + // Need to request permission first + pendingUpload = Pair(maxDownloads, expiryHours) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + } + // Show error toast LaunchedEffect(uiState.errorMessage) { uiState.errorMessage?.let { error -> @@ -173,9 +214,7 @@ fun GeneralUploadTab( } else { val maxDownloads = PreferencesUtil.getDefaultMaxDownloads(context) val expiryHours = PreferencesUtil.getDefaultExpiryHours(context) - uiState.selectedFile?.let { file -> - viewModel.uploadFile(file, maxDownloads, expiryHours) - } + uploadWithPermissionCheck(maxDownloads, expiryHours) } }, modifier = Modifier.fillMaxWidth(), @@ -205,9 +244,7 @@ fun GeneralUploadTab( UploadParamsDialog( onDismiss = { viewModel.hideUploadParamsDialog() }, onConfirm = { maxDownloads, expiryHours -> - uiState.selectedFile?.let { file -> - viewModel.uploadFile(file, maxDownloads, expiryHours) - } + uploadWithPermissionCheck(maxDownloads, expiryHours) } ) } diff --git a/app/src/main/java/com/mattintech/simplelogupload/util/PreferencesUtil.java b/app/src/main/java/com/mattintech/simplelogupload/util/PreferencesUtil.java index 6484f98..325cef9 100644 --- a/app/src/main/java/com/mattintech/simplelogupload/util/PreferencesUtil.java +++ b/app/src/main/java/com/mattintech/simplelogupload/util/PreferencesUtil.java @@ -14,6 +14,7 @@ public class PreferencesUtil { public static final String KEY_SERVER_PORT = "server_port"; public static final String KEY_CLIENT_KEY = "client_key"; public static final String KEY_SETUP_COMPLETED = "setup_completed"; + public static final String KEY_ONBOARDING_COMPLETED = "onboarding_completed"; public static final String KEY_DEFAULT_MAX_DOWNLOADS = "default_max_downloads"; public static final String KEY_DEFAULT_EXPIRY_HOURS = "default_expiry_hours"; public static final String KEY_ASK_BEFORE_UPLOAD = "ask_before_upload"; @@ -23,6 +24,7 @@ public class PreferencesUtil { private static final int DEFAULT_SERVER_PORT = 0; private static final String DEFAULT_CLIENT_KEY = ""; private static final boolean DEFAULT_SETUP_COMPLETED = false; + private static final boolean DEFAULT_ONBOARDING_COMPLETED = false; private static final int DEFAULT_MAX_DOWNLOADS = 5; private static final int DEFAULT_EXPIRY_HOURS = 24; private static final boolean DEFAULT_ASK_BEFORE_UPLOAD = true; @@ -132,6 +134,7 @@ public class PreferencesUtil { editor.putInt(KEY_SERVER_PORT, DEFAULT_SERVER_PORT); editor.putString(KEY_CLIENT_KEY, DEFAULT_CLIENT_KEY); editor.putBoolean(KEY_SETUP_COMPLETED, DEFAULT_SETUP_COMPLETED); + editor.putBoolean(KEY_ONBOARDING_COMPLETED, DEFAULT_ONBOARDING_COMPLETED); editor.putInt(KEY_DEFAULT_MAX_DOWNLOADS, DEFAULT_MAX_DOWNLOADS); editor.putInt(KEY_DEFAULT_EXPIRY_HOURS, DEFAULT_EXPIRY_HOURS); editor.putBoolean(KEY_ASK_BEFORE_UPLOAD, DEFAULT_ASK_BEFORE_UPLOAD); @@ -150,14 +153,34 @@ public class PreferencesUtil { /** * Set the setup completed status - * + * * @param context The application context * @param completed True if setup has been completed, false otherwise */ public static void setSetupCompleted(Context context, boolean completed) { getPreferences(context).edit().putBoolean(KEY_SETUP_COMPLETED, completed).apply(); } - + + /** + * Check if onboarding has been completed + * + * @param context The application context + * @return True if onboarding has been completed, false otherwise + */ + public static boolean isOnboardingCompleted(Context context) { + return getPreferences(context).getBoolean(KEY_ONBOARDING_COMPLETED, DEFAULT_ONBOARDING_COMPLETED); + } + + /** + * Set the onboarding completed status + * + * @param context The application context + * @param completed True if onboarding has been completed, false otherwise + */ + public static void setOnboardingCompleted(Context context, boolean completed) { + getPreferences(context).edit().putBoolean(KEY_ONBOARDING_COMPLETED, completed).apply(); + } + /** * Get the default max downloads from preferences *