diff --git a/app/build.gradle b/app/build.gradle index f79abf0de..eff156802 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,11 +105,14 @@ dependencies { // Annotation processors kapt common.glide_ap kapt common.eventbus_ap + kapt google.auto_service implementation common.editor implementation common.utilcode implementation common.glide implementation common.jsoup + + implementation google.auto_service_annotations implementation google.gson implementation google.guava diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 51f2d381d..7b2d38f3e 100755 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -26,6 +26,11 @@ -keep class org.w3c.** { *; } -keep class org.xml.** { *; } +# Services +-keepclassmembers class ** { + @com.google.auto.service.AutoService ; +} + # EventBus -keepattributes *Annotation* -keepclassmembers class ** { @@ -51,7 +56,7 @@ (...); } -keep class com.itsaky.androidide.editor.IEditor { *; } --keep class * extends com.itsaky.androidide.inflater.IAttributeAdapter { *; } +-keep class * extends com.itsaky.androidide.inflater.IViewAdapter { *; } -keep class * extends com.itsaky.androidide.inflater.drawable.IDrawableParser { (...); android.graphics.drawable.Drawable parse(); diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt index 58d499a4d..24880a0f5 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt @@ -72,6 +72,8 @@ import com.itsaky.androidide.lookup.Lookup import com.itsaky.androidide.lsp.models.DiagnosticItem import com.itsaky.androidide.models.DiagnosticGroup import com.itsaky.androidide.models.LogLine +import com.itsaky.androidide.models.OpenedFile +import com.itsaky.androidide.models.OpenedFilesCache import com.itsaky.androidide.models.Range import com.itsaky.androidide.models.SearchResult import com.itsaky.androidide.projects.ProjectManager.getProjectDirPath @@ -159,6 +161,7 @@ abstract class BaseEditorActivity : protected abstract fun doSaveAll(): Boolean protected abstract fun doDismissSearchProgress() protected abstract fun doConfirmProjectClose() + protected abstract fun getOpenedFiles() : List protected open fun preDestroy() { viewModel.isConfigChange = !isDestroying @@ -489,7 +492,7 @@ abstract class BaseEditorActivity : viewModel._isBuildInProgress.observe(this) { onBuildStatusChanged() } viewModel._isInitializing.observe(this) { onBuildStatusChanged() } viewModel._statusText.observe(this) { binding.bottomSheet.setStatus(it.first, it.second) } - + viewModel.observeFiles(this) { files -> binding.apply { if (files == null || files.isEmpty()) { @@ -499,9 +502,12 @@ abstract class BaseEditorActivity : tabs.visibility = View.VISIBLE viewContainer.displayedChild = 0 } + + val currentFile = provideCurrentEditor()?.editor?.file?.absolutePath ?: return@observeFiles + getOpenedFiles().also { viewModel.writeOpenedFiles(OpenedFilesCache(currentFile, it)) } } } - + setupNoEditorView() setupBottomSheet() diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt index 637975ba4..2ebb37978 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt @@ -22,6 +22,7 @@ import android.content.Context import android.os.Bundle import android.text.TextUtils import android.view.Menu +import android.view.View import android.view.ViewGroup.LayoutParams import androidx.appcompat.view.menu.MenuBuilder import androidx.core.view.GravityCompat @@ -33,6 +34,8 @@ import com.itsaky.androidide.actions.ActionItem.Location.EDITOR_TOOLBAR import com.itsaky.androidide.actions.ActionsRegistry.Companion.getInstance import com.itsaky.androidide.editor.ui.IDEEditor import com.itsaky.androidide.interfaces.IEditorHandler +import com.itsaky.androidide.models.OpenedFile +import com.itsaky.androidide.models.OpenedFilesCache import com.itsaky.androidide.models.Range import com.itsaky.androidide.models.SaveResult import com.itsaky.androidide.projects.ProjectManager.generateSources @@ -72,7 +75,13 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { return getEditorAtIndex(index) } + override fun preDestroy() { + super.preDestroy() + viewModel.removeAllFiles() + } + override fun onCreate(savedInstanceState: Bundle?) { + mBuildEventListener.setActivity(this) super.onCreate(savedInstanceState) viewModel._displayedFile.observe(this) { this.binding.editorContainer.displayedChild = it } @@ -81,15 +90,6 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { if (opened) openDrawer(GravityCompat.END) else closeDrawer(GravityCompat.END) } } - - if (viewModel.getOpenedFileCount() != binding.tabs.tabCount) { - // The activity proabably has been restarted after a configuration change - // restore the opened files - - val files = viewModel.getOpenedFiles() - viewModel.removeAllFiles() - files.forEach(this::openFile) - } } @SuppressLint("RestrictedApi") @@ -100,6 +100,39 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { return true } + override fun onPause() { + super.onPause() + + val current = getCurrentEditor()?.editor?.file?.absolutePath ?: return + getOpenedFiles().also { + val cache = OpenedFilesCache(current, it) + viewModel.writeOpenedFiles(cache) + viewModel.openedFilesCache = if (!isFinishing) cache else null + } + } + + override fun onStart() { + super.onStart() + + try { + log.debug("try to reopen recently opened files") + if (viewModel.openedFilesCache != null) { + viewModel.openedFilesCache?.apply { + this.allFiles.forEach { openFile(File(it.filePath), it.selection) } + openFile(File(selectedFile)) + } + } else { + viewModel.readOpenedFiles { + it ?: return@readOpenedFiles + it.allFiles.forEach { file -> openFile(File(file.filePath), file.selection) } + openFile(File(it.selectedFile)) + } + } + } catch (err: Throwable) { + log.error("Failed to reopen recently opened files", err) + } + } + override fun onPrepareOptionsMenu(menu: Menu): Boolean { ensureToolbarMenu(menu) return true @@ -159,10 +192,13 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { override fun openFileAndGetIndex(file: File, selection: Range?): Int { val openedFileIndex = findIndexOfEditorByFile(file) if (openedFileIndex != -1) { - log.error("File is already opened. File: $file") return openedFileIndex } + if (!file.exists()) { + return -1 + } + val position = viewModel.getOpenedFileCount() log.info("Opening file at index:", position, "file: ", file) @@ -387,6 +423,12 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler { runAfter() } + override fun getOpenedFiles() = + viewModel.getOpenedFiles().mapNotNull { + val editor = getEditorForFile(it)?.editor ?: return@mapNotNull null + OpenedFile(it.absolutePath, editor.cursorLSPRange) + } + private fun notifyFilesUnsaved(unsavedEditors: List, invokeAfter: Runnable) { if (isDestroying) { // Do not show unsaved files dialog if the activity is being destroyed diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt index 2052709de..dc17a3899 100644 --- a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt +++ b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt @@ -53,6 +53,7 @@ import com.itsaky.androidide.utils.RecursiveFileSearcher import com.itsaky.toaster.Toaster.Type.ERROR import com.itsaky.toaster.toast import java.io.File +import java.util.ServiceLoader import java.util.concurrent.CompletableFuture import java.util.regex.Pattern import java.util.stream.Collectors @@ -92,8 +93,6 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - mBuildEventListener.setActivity(this as EditorHandlerActivity) startServices() } @@ -238,6 +237,11 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { notifyProjectUpdate() ThreadUtils.runOnUiThread { postProjectInit() } } + + protected open fun preProjectInit() { + setStatus(getString(string.msg_initializing_project)) + viewModel.isInitializing = true + } protected open fun postProjectInit() { initialSetup() @@ -403,11 +407,6 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { connectClient(IDELanguageClientImpl.getInstance()) } - private fun preProjectInit() { - setStatus(getString(string.msg_initializing_project)) - viewModel.isInitializing = true - } - open fun getProgressSheet(msg: Int): ProgressSheet? { doDismissSearchProgress() diff --git a/app/src/main/java/com/itsaky/androidide/models/OpenedFile.kt b/app/src/main/java/com/itsaky/androidide/models/OpenedFile.kt new file mode 100644 index 000000000..0853914f9 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/models/OpenedFile.kt @@ -0,0 +1,30 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.models + +import com.google.gson.annotations.SerializedName + +/** + * A file that is opened in editor. + * + * @author Akash Yadav + */ +data class OpenedFile( + @SerializedName("file") val filePath: String, + @SerializedName("selection") var selection: Range +) diff --git a/app/src/main/java/com/itsaky/androidide/models/OpenedFilesCache.kt b/app/src/main/java/com/itsaky/androidide/models/OpenedFilesCache.kt new file mode 100644 index 000000000..448cb70f1 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/models/OpenedFilesCache.kt @@ -0,0 +1,23 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.models + +/** + * @author Akash Yadav + */ +data class OpenedFilesCache(val selectedFile: String, val allFiles: List) \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/EditorViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/EditorViewModel.kt index f9f995bef..24dea4ebd 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/EditorViewModel.kt +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/EditorViewModel.kt @@ -22,6 +22,13 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel +import com.blankj.utilcode.util.FileUtils +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.itsaky.androidide.models.OpenedFilesCache +import com.itsaky.androidide.projects.ProjectManager +import com.itsaky.androidide.tasks.executeAsync +import com.itsaky.androidide.utils.Environment import java.io.File /** ViewModel for data used in [com.itsaky.androidide.activities.editor.EditorActivityKt] */ @@ -33,10 +40,12 @@ class EditorViewModel : ViewModel() { internal val _statusText = MutableLiveData>("" to CENTER) internal val _displayedFile = MutableLiveData(-1) internal val _fileTreeDrawerOpened = MutableLiveData(false) - + + private val _openedFiles = MutableLiveData() private val _isBoundToBuildService = MutableLiveData(false) private val _isConfigurationChange = MutableLiveData(false) private val files = MutableLiveData>(ArrayList()) + private val fileModified = MutableLiveData(false) /** @@ -45,6 +54,12 @@ class EditorViewModel : ViewModel() { */ private val mCurrentFile = MutableLiveData?>(null) + var openedFilesCache: OpenedFilesCache? + get() = _openedFiles.value + set(value) { + this._openedFiles.value = value + } + var isBoundToBuildSerice: Boolean get() = _isBoundToBuildService.value ?: false set(value) { @@ -179,4 +194,40 @@ class EditorViewModel : ViewModel() { val modified = fileModified.value return modified != null && modified } + + fun readOpenedFiles(result: (OpenedFilesCache?) -> Unit) { + executeAsync({ + val file = getOpenedFilesCache() + if (file.length() == 0L) { + return@executeAsync null + } + return@executeAsync Gson().fromJson(file.readText(), OpenedFilesCache::class.java) + }) { + result(it) + } + } + + fun writeOpenedFiles(cache: OpenedFilesCache) { + executeAsync { + val file = getOpenedFilesCache() + val gson = GsonBuilder().setPrettyPrinting().create() + val string = gson.toJson(cache) + file.writeText(string) + } + } + + private fun getOpenedFilesCache(): File { + var file = Environment.getProjectCacheDir(ProjectManager.projectPath) + file = File(file, "editor/openedFiles.json") + if (file.exists()) { + FileUtils.rename(file, "${file.name}.bak") + file.createNewFile() + } + + if (file.parentFile?.exists() == false) { + file.parentFile?.mkdirs() + } + + return file + } } diff --git a/common/src/main/java/com/itsaky/androidide/utils/Environment.java b/common/src/main/java/com/itsaky/androidide/utils/Environment.java index 42481357d..eed277cb4 100755 --- a/common/src/main/java/com/itsaky/androidide/utils/Environment.java +++ b/common/src/main/java/com/itsaky/androidide/utils/Environment.java @@ -44,6 +44,7 @@ public final class Environment { private static final String DEFAULT_ANDROID_HOME = DEFAULT_HOME + "/android-sdk"; public static final String DEFAULT_PREFIX = DEFAULT_ROOT + "/usr"; private static final String DEFAULT_JAVA_HOME = DEFAULT_PREFIX + "/opt/openjdk"; + private static final String ANDROIDIDE_PROJECT_CACHE_DIR = ".androidide"; private static final ILogger LOG = ILogger.newInstance("Environment"); private static final List blacklist = new ArrayList<>(); public static File ROOT; @@ -214,7 +215,7 @@ public final class Environment { ENV_VARS.put("SHELL", SHELL.getAbsolutePath()); ENV_VARS.put("CONFIG_SHELL", SHELL.getAbsolutePath()); ENV_VARS.put("TERM", "screen"); - + // https://github.com/termux/termux-tools/blob/f2736f7f8232cd19cf52bca9b0ac9afb8ad9e562/scripts/termux-setup-package-manager.in#L3 ENV_VARS.put("TERMUX_APP_PACKAGE_MANAGER", "apt"); ENV_VARS.put("TERMUX_PKG_NO_MIRROR_SELECT", "true"); @@ -262,4 +263,12 @@ public final class Environment { } return blacklist; } + + public static File getProjectCacheDir(String projectDir) { + return new File(projectDir, ANDROIDIDE_PROJECT_CACHE_DIR); + } + + public static File getProjectCacheDir(File projectDir) { + return new File(projectDir, ANDROIDIDE_PROJECT_CACHE_DIR); + } } diff --git a/dependencies.gradle b/dependencies.gradle index 71cae12b9..a56715f6a 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -58,15 +58,16 @@ ext { ] google = [ - material : 'com.google.android.material:material:1.7.0', - gson : 'com.google.code.gson:gson:2.10', - guava : 'com.google.guava:guava:31.1-android', - guava_jre : 'com.google.guava:guava:31.1-jre', - auto_val_annotations: 'com.google.auto.value:auto-value-annotations:1.10.1', - auto_val_ap : 'com.google.auto.value:auto-value:1.10', - auto_service : 'com.google.auto.service:auto-service:1.0.1', - oss_licenses : 'com.google.android.gms:play-services-oss-licenses:17.0.0', - protobuf : 'com.google.protobuf:protobuf-java:3.10.0' + material : 'com.google.android.material:material:1.7.0', + gson : 'com.google.code.gson:gson:2.10', + guava : 'com.google.guava:guava:31.1-android', + guava_jre : 'com.google.guava:guava:31.1-jre', + auto_val_annotations : 'com.google.auto.value:auto-value-annotations:1.10.1', + auto_val_ap : 'com.google.auto.value:auto-value:1.10', + auto_service_annotations: 'com.google.auto.service:auto-service-annotations:1.0.1', + auto_service : 'com.google.auto.service:auto-service:1.0.1', + oss_licenses : 'com.google.android.gms:play-services-oss-licenses:17.0.0', + protobuf : 'com.google.protobuf:protobuf-java:3.10.0' ] aapt = [ diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java index e90d2ce2c..a789d9064 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java +++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.java @@ -81,8 +81,7 @@ public class IDEEditor extends CodeEditor implements com.itsaky.androidide.edito private ILanguageServer languageServer; private SignatureHelpWindow signatureHelpWindow; private DiagnosticWindow diagnosticWindow; - @Nullable - ILanguageClient languageClient; + @Nullable ILanguageClient languageClient; public IDEEditor(Context context) { this(context, null); @@ -413,17 +412,17 @@ public class IDEEditor extends CodeEditor implements com.itsaky.androidide.edito @Override public void setLanguageServer(ILanguageServer server) { this.languageServer = server; - + if (server != null) { this.languageClient = server.getClient(); } } - + @Override public void setLanguageClient(@Nullable final ILanguageClient languageClient) { this.languageClient = languageClient; } - + /** * Append the given text at the end of the editor's content. * @@ -823,7 +822,7 @@ public class IDEEditor extends CodeEditor implements com.itsaky.androidide.edito return; } - final var closeEvent = new DocumentCloseEvent(getFile().toPath()); + final var closeEvent = new DocumentCloseEvent(getFile().toPath(), getCursorLSPRange()); EventBus.getDefault().post(closeEvent); } diff --git a/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/editor/EditorEvents.kt b/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/editor/EditorEvents.kt index 8130e997b..c14b1c902 100644 --- a/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/editor/EditorEvents.kt +++ b/eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/editor/EditorEvents.kt @@ -33,7 +33,10 @@ data class DocumentOpenEvent(var openedFile: Path, var text: String, var version DocumentEvent(openedFile) /** Dispatched when the given file is closed. Always dispatched after [DocumentOpenEvent]. */ -data class DocumentCloseEvent(var closedFile: Path) : DocumentEvent(closedFile) +data class DocumentCloseEvent +@JvmOverloads +constructor(var closedFile: Path, val selectionRange: Range = Range.NONE) : + DocumentEvent(closedFile) /** * Dispatched when the content of the given opened document changes. The change can be either diff --git a/tests/test-home/.androidide/init/model.jar b/tests/test-home/.androidide/init/model.jar index 8f9df10c6..be6127240 100644 Binary files a/tests/test-home/.androidide/init/model.jar and b/tests/test-home/.androidide/init/model.jar differ