diff --git a/app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt index ee615d4ff6..ac4606a9ed 100755 --- a/app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt @@ -19,7 +19,6 @@ package com.itsaky.androidide.activities import android.content.Intent import android.os.Bundle -import android.text.TextUtils import android.view.View import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels @@ -46,6 +45,7 @@ import com.itsaky.androidide.utils.DialogUtils import com.itsaky.androidide.utils.Environment import com.itsaky.androidide.utils.FeatureFlags import com.itsaky.androidide.utils.UrlManager +import com.itsaky.androidide.utils.findValidProjects import com.itsaky.androidide.utils.flashInfo import com.itsaky.androidide.viewmodel.MainViewModel import com.itsaky.androidide.viewmodel.MainViewModel.Companion.SCREEN_DELETE_PROJECTS @@ -56,6 +56,7 @@ import com.itsaky.androidide.viewmodel.MainViewModel.Companion.SCREEN_TEMPLATE_L import com.itsaky.androidide.viewmodel.MainViewModel.Companion.TOOLTIPS_WEB_VIEW import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.appdevforall.localwebserver.ServerConfig import org.appdevforall.localwebserver.WebServer import org.koin.android.ext.android.inject @@ -104,7 +105,7 @@ class MainActivity : EdgeToEdgeIDEActivity() { // Start WebServer after installation is complete startWebServer() - openLastProject() + if (savedInstanceState == null) { openLastProject() } if (FeatureFlags.isExperimentsEnabled) { binding.codeOnTheGoLabel.title = getString(R.string.app_name) + "." @@ -259,32 +260,37 @@ class MainActivity : EdgeToEdgeIDEActivity() { } private fun tryOpenLastProject() { - if (!GeneralPreferences.autoOpenProjects) { - return - } + if (!GeneralPreferences.autoOpenProjects) return - val openedProject = GeneralPreferences.lastOpenedProject - if (GeneralPreferences.NO_OPENED_PROJECT == openedProject) { - return - } + lifecycleScope.launch(Dispatchers.IO) { + val validProjects = findValidProjects(Environment.PROJECTS_DIR) + val lastOpenedPath = GeneralPreferences.lastOpenedProject - if (TextUtils.isEmpty(openedProject)) { - flashInfo(string.msg_opened_project_does_not_exist) - return - } + val projectToOpen = validProjects.find { it.absolutePath == lastOpenedPath } + ?: validProjects.maxByOrNull { it.lastModified() } - val project = File(openedProject) - if (!project.exists()) { - flashInfo(string.msg_opened_project_does_not_exist) - return + withContext(Dispatchers.Main) { + when { + projectToOpen != null -> handleOpenProject(projectToOpen) + + lastOpenedPath.isNotBlank() && lastOpenedPath != GeneralPreferences.NO_OPENED_PROJECT -> { + if (!File(lastOpenedPath).exists()) { + flashInfo(string.msg_opened_project_does_not_exist) + } + } + + else -> Unit + } + } } + } + private fun handleOpenProject(root: File) { if (GeneralPreferences.confirmProjectOpen) { - askProjectOpenPermission(project) + askProjectOpenPermission(root) return } - - openProject(project) + openProject(root) } private fun askProjectOpenPermission(root: File) { diff --git a/app/src/main/java/com/itsaky/androidide/fragments/RecentProjectsFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/RecentProjectsFragment.kt index 077da80573..ae5e594e9d 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/RecentProjectsFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/RecentProjectsFragment.kt @@ -28,7 +28,6 @@ import com.itsaky.androidide.utils.flashError import com.itsaky.androidide.utils.viewLifecycleScope import com.itsaky.androidide.viewmodel.MainViewModel import com.itsaky.androidide.viewmodel.RecentProjectsViewModel -import com.itsaky.androidide.preferences.internal.GeneralPreferences import com.itsaky.androidide.viewmodel.SortCriteria import com.itsaky.androidide.ui.ProjectInfoBottomSheet import io.sentry.Sentry @@ -37,9 +36,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.appdevforall.codeonthego.layouteditor.ProjectFile import com.itsaky.androidide.utils.flashSuccess +import com.itsaky.androidide.utils.findValidProjects +import com.itsaky.androidide.utils.isProjectCandidateDir +import com.itsaky.androidide.utils.isValidProjectDirectory +import com.itsaky.androidide.utils.isValidProjectOrContainerDirectory import java.io.File class RecentProjectsFragment : BaseFragment() { @@ -219,7 +221,7 @@ class RecentProjectsFragment : BaseFragment() { } - private fun File.isProjectCandidateDir(): Boolean = isDirectory && canRead() && !name.startsWith(".") && !isHidden + private fun bootstrapFromFixedFolderIfNeeded() { if (viewModel.didBootstrap) return @@ -231,42 +233,12 @@ class RecentProjectsFragment : BaseFragment() { if (validProjects.isEmpty()) return@launch loadProjectsIntoViewModel(validProjects) - - if (GeneralPreferences.autoOpenProjects) { - val lastOpenedPath = GeneralPreferences.lastOpenedProject - - val projectToOpen = validProjects.find { - it.absolutePath == lastOpenedPath - } - - if (projectToOpen != null) { - withContext(Dispatchers.Main) { openProject(projectToOpen) } - return@launch - } - - val lastCreated = validProjects.maxByOrNull { it.lastModified() } - - if (lastCreated != null) { - withContext(Dispatchers.Main) { openProject(lastCreated) } - } - } } catch (e: Throwable) { Sentry.captureException(e) } } } - private fun findValidProjects(projectsRoot: File): List { - if (!projectsRoot.isProjectCandidateDir()) return emptyList() - - val subdirs = projectsRoot.listFiles() - ?.filter { it.isProjectCandidateDir() } - .orEmpty() - if (subdirs.isEmpty()) return emptyList() - - return subdirs.filter { dir -> isValidProjectDirectory(dir) } - } - private suspend fun loadProjectsIntoViewModel(projects: List) { val jobs = projects.map { dir -> viewModel.insertProjectFromFolder(dir.name, dir.absolutePath) @@ -396,43 +368,6 @@ class RecentProjectsFragment : BaseFragment() { } } - fun isValidProjectDirectory(selectedDir: File): Boolean { - if (isPluginProject(selectedDir)) { - return true - } - - val appFolder = File(selectedDir, "app") - val buildGradleFile = File(appFolder, "build.gradle") - val buildGradleKtsFile = File(appFolder, "build.gradle.kts") - return appFolder.exists() && appFolder.isDirectory && - (buildGradleFile.exists() || buildGradleKtsFile.exists()) - } - - private fun isPluginProject(dir: File): Boolean { - val pluginApiJar = File(dir, "libs/plugin-api.jar") - val buildGradle = File(dir, "build.gradle.kts") - return pluginApiJar.exists() && buildGradle.exists() - } - - /** - * Determines if the selected directory is either: - * 1. A valid Android project itself, OR - * 2. A container that includes one or more valid Android projects. - */ - fun isValidProjectOrContainerDirectory(selectedDir: File): Boolean { - if (!selectedDir.isProjectCandidateDir()) { - return false - } - - if (isValidProjectDirectory(selectedDir)) { - return true - } - - // Check if it contains valid Android projects as subdirectories - val subDirs = selectedDir.listFiles()?.filter { it.isProjectCandidateDir() } ?: return false - return subDirs.any { sub -> isValidProjectDirectory(sub) } - } - private fun openProjectInfo(project: ProjectFile) { viewLifecycleScope.launch { val recentProject = viewModel.getProjectByName(project.name) diff --git a/app/src/main/java/com/itsaky/androidide/utils/ProjectValidations.kt b/app/src/main/java/com/itsaky/androidide/utils/ProjectValidations.kt new file mode 100644 index 0000000000..4859e048c8 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/utils/ProjectValidations.kt @@ -0,0 +1,59 @@ +package com.itsaky.androidide.utils + +import java.io.File +import kotlin.collections.filter +import kotlin.collections.orEmpty + +/** Checks if the file is a readable, visible directory. */ +internal fun File.isProjectCandidateDir(): Boolean = isDirectory && canRead() && !name.startsWith(".") && !isHidden + +/** Scans the given root directory for valid Android project subdirectories. */ +internal fun findValidProjects(projectsRoot: File): List { + if (!projectsRoot.isProjectCandidateDir()) return emptyList() + + val subdirs = projectsRoot.listFiles() + ?.filter { it.isProjectCandidateDir() } + .orEmpty() + if (subdirs.isEmpty()) return emptyList() + + return subdirs.filter { dir -> isValidProjectDirectory(dir) } +} + +/** Determines if the directory contains a valid Android project structure. */ +fun isValidProjectDirectory(selectedDir: File): Boolean { + if (isPluginProject(selectedDir)) { + return true + } + + val appFolder = File(selectedDir, "app") + val buildGradleFile = File(appFolder, "build.gradle") + val buildGradleKtsFile = File(appFolder, "build.gradle.kts") + return appFolder.exists() && appFolder.isDirectory && + (buildGradleFile.exists() || buildGradleKtsFile.exists()) +} + +/** + * Determines if the selected directory is either: + * 1. A valid Android project itself, OR + * 2. A container that includes one or more valid Android projects. + */ +internal fun isValidProjectOrContainerDirectory(selectedDir: File): Boolean { + if (!selectedDir.isProjectCandidateDir()) { + return false + } + + if (isValidProjectDirectory(selectedDir)) { + return true + } + + // Check if it contains valid Android projects as subdirectories + val subDirs = selectedDir.listFiles()?.filter { it.isProjectCandidateDir() } ?: return false + return subDirs.any { sub -> isValidProjectDirectory(sub) } +} + +/** Checks if the directory contains a specific plugin project structure. */ +internal fun isPluginProject(dir: File): Boolean { + val pluginApiJar = File(dir, "libs/plugin-api.jar") + val buildGradle = File(dir, "build.gradle.kts") + return pluginApiJar.exists() && buildGradle.exists() +} \ No newline at end of file