Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) + "."
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand Down Expand Up @@ -219,7 +221,7 @@ class RecentProjectsFragment : BaseFragment() {
}


private fun File.isProjectCandidateDir(): Boolean = isDirectory && canRead() && !name.startsWith(".") && !isHidden


private fun bootstrapFromFixedFolderIfNeeded() {
if (viewModel.didBootstrap) return
Expand All @@ -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<File> {
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<File>) {
val jobs = projects.map { dir ->
viewModel.insertProjectFromFolder(dir.name, dir.absolutePath)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<File> {
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()
}