diff --git a/app/src/main/assets/data/common/tooling-api-all.jar b/app/src/main/assets/data/common/tooling-api-all.jar index fdc50f79f..31fc643e9 100644 Binary files a/app/src/main/assets/data/common/tooling-api-all.jar and b/app/src/main/assets/data/common/tooling-api-all.jar differ 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 4915c9fb2..672bd7334 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 @@ -253,8 +253,15 @@ abstract class BaseEditorActivity : flashError(string.msg_failed_list_files) } } - + + override fun onStop() { + super.onStop() + + checkIsDestroying() + } + override fun onDestroy() { + checkIsDestroying() preDestroy() super.onDestroy() postDestroy() @@ -424,6 +431,12 @@ abstract class BaseEditorActivity : viewModel.statusText = text viewModel.statusGravity = gravity } + + private fun checkIsDestroying() { + if (!isDestroying && isFinishing) { + isDestroying = true + } + } private fun handleUiDesignerResult(result: ActivityResult) { if (result.resultCode != RESULT_OK || result.data == null) { 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 ffe2e21f8..3cd774959 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 @@ -137,14 +137,27 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { } super.preDestroy() - + if (isDestroying) { + + if (IDELanguageClientImpl.isInitialized()) { + IDELanguageClientImpl.shutdown() + } + + try { + stopLanguageServers() + } catch (err: Exception) { + log.error("Failed to stop editor services.") + } + try { unbindService(buildServiceConnection) + buildServiceConnection.onConnected = {} } catch (err: Throwable) { log.error("Unable to unbind service") } finally { - (Lookup.DEFAULT.lookup(BuildService.KEY_BUILD_SERVICE) as? GradleBuildService?)?.setEventListener(null) + (Lookup.DEFAULT.lookup(BuildService.KEY_BUILD_SERVICE) as? GradleBuildService?) + ?.setEventListener(null) Lookup.DEFAULT.unregister(BuildService.KEY_BUILD_SERVICE) viewModel.isBoundToBuildSerice = false } @@ -240,11 +253,8 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { } } - override fun stopServices() { + override fun stopLanguageServers() { try { - if (IDELanguageClientImpl.isInitialized()) { - IDELanguageClientImpl.shutdown() - } destroyLanguageServers(isChangingConfigurations) } catch (err: Throwable) { log.error("Unable to stop editor services. Please report this issue.", err) @@ -415,8 +425,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler { } private fun closeProject(manualFinish: Boolean) { - stopServices() - + // Make sure we close files // This will make sure that file contents are not erased. doCloseAll { diff --git a/app/src/main/java/com/itsaky/androidide/interfaces/IProjectHandler.kt b/app/src/main/java/com/itsaky/androidide/interfaces/IProjectHandler.kt index 0839cbfe0..4fe4c63c5 100644 --- a/app/src/main/java/com/itsaky/androidide/interfaces/IProjectHandler.kt +++ b/app/src/main/java/com/itsaky/androidide/interfaces/IProjectHandler.kt @@ -34,5 +34,5 @@ interface IProjectHandler { fun startServices() fun initializeProject() - fun stopServices() + fun stopLanguageServers() } \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java b/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java index 1b3a413e8..7b6586a28 100755 --- a/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java +++ b/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java @@ -103,6 +103,9 @@ public class IDELanguageClientImpl implements ILanguageClient { } public static void shutdown() { + if (mInstance != null) { + mInstance.activity = null; + } mInstance = null; } @@ -308,7 +311,7 @@ public class IDELanguageClientImpl implements ILanguageClient { private List mapAsGroup(Map> map) { final var groups = new ArrayList(); var diagnosticMap = map; - if (diagnosticMap == null || diagnosticMap.size() <= 0) return groups; + if (diagnosticMap == null || diagnosticMap.size() == 0) return groups; if (diagnosticMap.size() > 10) { LOG.warn("Limiting the diagnostics to 10 files"); @@ -317,7 +320,7 @@ public class IDELanguageClientImpl implements ILanguageClient { for (File file : diagnosticMap.keySet()) { var fileDiagnostics = diagnosticMap.get(file); - if (fileDiagnostics == null || fileDiagnostics.size() <= 0) continue; + if (fileDiagnostics == null || fileDiagnostics.size() == 0) continue; // Trim the diagnostics list if we have too many diagnostic items. // Including a lot of diagnostic items will result in UI lag when they are shown diff --git a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.java b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.java index 5f70cab28..413ca419b 100644 --- a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.java +++ b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildService.java @@ -34,7 +34,6 @@ import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; -import android.os.Binder; import android.os.IBinder; import android.text.TextUtils; @@ -53,6 +52,7 @@ import com.itsaky.androidide.projects.builder.BuildService; import com.itsaky.androidide.resources.R; import com.itsaky.androidide.services.ToolingServerNotStartedException; import com.itsaky.androidide.shell.ProcessStreamsHolder; +import com.itsaky.androidide.tooling.api.ForwardingToolingApiClient; import com.itsaky.androidide.tooling.api.IProject; import com.itsaky.androidide.tooling.api.IToolingApiClient; import com.itsaky.androidide.tooling.api.IToolingApiServer; @@ -66,7 +66,6 @@ import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult; import com.itsaky.androidide.tooling.events.ProgressEvent; import com.itsaky.androidide.utils.Environment; import com.itsaky.androidide.utils.ILogger; -import com.itsaky.androidide.utils.InputStreamLineReader; import org.jetbrains.annotations.NotNull; @@ -91,10 +90,18 @@ public class GradleBuildService extends Service implements BuildService, IToolin private static final int NOTIFICATION_ID = R.string.app_name; private static final ILogger SERVER_System_err = newInstance("ToolingApiErrorStream"); private final ILogger SERVER_LOGGER = newInstance("ToolingApiServer"); - private final IBinder mBinder = new GradleServiceBinder(); + private final IBinder mBinder = new GradleServiceBinder(this); private boolean isToolingServerStarted = false; private boolean isBuildInProgress = false; + + /** + * We do not provide direct access to GradleBuildService instance to the Tooling API launcher as + * it may cause memory leaks. Instead, we create another client object which forwards all calls to us. + * So, when the service is destroyed, we release the reference to the service from this client. + */ + private ForwardingToolingApiClient _toolingApiClient; private ToolingServerRunner toolingServerThread; + private OutputReaderThread outputReader; private NotificationManager notificationManager; private IToolingApiServer server; @Nullable @@ -156,11 +163,32 @@ public class GradleBuildService extends Service implements BuildService, IToolin notificationManager.cancel(NOTIFICATION_ID); if (server != null) { server.cancelCurrentBuild(); - server.shutdown(); + final var shutdown = server.shutdown(); + + //noinspection ConstantConditions + if (shutdown != null) { + try { + LOG.info("Shutting down Tooling API server..."); + shutdown.get(); + } catch (Throwable e) { + LOG.error("Failed to shutdown Tooling API server", e); + } + } } if (toolingServerThread != null) { toolingServerThread.interrupt(); + toolingServerThread = null; + } + + if (_toolingApiClient != null) { + _toolingApiClient.setClient(null); + _toolingApiClient = null; + } + + if (outputReader != null) { + outputReader.interrupt(); + outputReader = null; } isToolingServerStarted = false; @@ -191,7 +219,10 @@ public class GradleBuildService extends Service implements BuildService, IToolin @NonNull @Override public IToolingApiClient getClient() { - return this; + if (_toolingApiClient == null) { + _toolingApiClient = new ForwardingToolingApiClient(this); + } + return _toolingApiClient; } @Override @@ -487,19 +518,15 @@ public class GradleBuildService extends Service implements BuildService, IToolin } private void startServerOutputReader(InputStream err) { - new Thread(new InputStreamLineReader(err, this::onServerOutput)).start(); - } - - protected void onServerOutput(String line) { - SERVER_System_err.error(line); - } - - public class GradleServiceBinder extends Binder { - public GradleBuildService getService() { - return GradleBuildService.this; + if (outputReader == null) { + outputReader = new OutputReaderThread(err, SERVER_System_err::error); + } + + if (!outputReader.isAlive()) { + outputReader.start(); } } - + /** Handles events received from a Gradle build. */ public interface EventListener { diff --git a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildServiceConnnection.kt b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildServiceConnnection.kt index e15b0effa..f10f7c2ca 100644 --- a/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildServiceConnnection.kt +++ b/app/src/main/java/com/itsaky/androidide/services/builder/GradleBuildServiceConnnection.kt @@ -20,7 +20,6 @@ package com.itsaky.androidide.services.builder import android.content.ComponentName import android.content.ServiceConnection import android.os.IBinder -import com.itsaky.androidide.services.builder.GradleBuildService.GradleServiceBinder import com.itsaky.androidide.utils.ILogger /** @@ -34,8 +33,8 @@ class GradleBuildServiceConnnection : ServiceConnection { private val log = ILogger.newInstance("GradleBuildServiceConnnection") override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - val buildService = (service as GradleServiceBinder).service - onConnected(buildService) + val serviceBinder = service as GradleServiceBinder + onConnected(serviceBinder.service!!) } override fun onServiceDisconnected(name: ComponentName?) { diff --git a/app/src/main/java/com/itsaky/androidide/services/builder/GradleServiceBinder.kt b/app/src/main/java/com/itsaky/androidide/services/builder/GradleServiceBinder.kt new file mode 100644 index 000000000..7fda46adb --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/services/builder/GradleServiceBinder.kt @@ -0,0 +1,41 @@ +/* + * 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.services.builder + +import android.os.Binder + +/** + * [Binder] implementation for [GradleBuildService] which provides a single-use instance of the + * service. + * + * @author Akash Yadav + */ +internal class GradleServiceBinder(service: GradleBuildService?) : Binder() { + + private var released = false + var service: GradleBuildService? = service + private set + get() { + check(!released) { "GradleBuildService instance has been released" } + return field.also { release() } + } + + private fun release() { + service = null + released = true + } +} diff --git a/app/src/main/java/com/itsaky/androidide/services/builder/OutputReaderThread.kt b/app/src/main/java/com/itsaky/androidide/services/builder/OutputReaderThread.kt new file mode 100644 index 000000000..3632f9fd8 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/services/builder/OutputReaderThread.kt @@ -0,0 +1,45 @@ +/* + * 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.services.builder + +import java.io.InputStream +import java.util.function.Consumer + +/** + * Reads output from the Tooling API server. + * + * @author Akash Yadav + */ +class OutputReaderThread(private val input: InputStream, private var onRead: Consumer?) : + Thread("ToolingServerOutputReader") { + + override fun run() { + input.bufferedReader().use { + var line: String? = it.readLine() + while (line != null) { + onRead?.accept(line) + line = it.readLine() + } + } + } + + override fun interrupt() { + super.interrupt() + onRead = null + } +} diff --git a/app/src/main/java/com/itsaky/androidide/services/builder/ToolingServerRunner.kt b/app/src/main/java/com/itsaky/androidide/services/builder/ToolingServerRunner.kt index 1df167106..3c1f008da 100644 --- a/app/src/main/java/com/itsaky/androidide/services/builder/ToolingServerRunner.kt +++ b/app/src/main/java/com/itsaky/androidide/services/builder/ToolingServerRunner.kt @@ -123,6 +123,7 @@ internal class ToolingServerRunner( override fun interrupt() { super.interrupt() + listener = null observer = null } diff --git a/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/ForwardingToolingApiClient.kt b/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/ForwardingToolingApiClient.kt new file mode 100644 index 000000000..443284a23 --- /dev/null +++ b/subprojects/tooling-api/src/main/java/com/itsaky/androidide/tooling/api/ForwardingToolingApiClient.kt @@ -0,0 +1,65 @@ +/* + * 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.tooling.api + +import com.itsaky.androidide.models.LogLine +import com.itsaky.androidide.tooling.api.messages.result.BuildResult +import com.itsaky.androidide.tooling.api.messages.result.GradleWrapperCheckResult +import com.itsaky.androidide.tooling.events.ProgressEvent +import java.util.concurrent.CompletableFuture + +/** + * A [IToolingApiClient] which forwards all of its calls to the given client. + * + * @author Akash Yadav + */ +class ForwardingToolingApiClient(var client: IToolingApiClient?) : IToolingApiClient { + + override fun logMessage(line: LogLine) { + client?.logMessage(line) + } + + override fun logOutput(line: String) { + client?.logOutput(line) + } + + override fun prepareBuild() { + client?.prepareBuild() + } + + override fun onBuildSuccessful(result: BuildResult) { + client?.onBuildSuccessful(result) + } + + override fun onBuildFailed(result: BuildResult) { + client?.onBuildFailed(result) + } + + override fun onProgressEvent(event: ProgressEvent) { + client?.onProgressEvent(event) + } + + override fun getBuildArguments(): CompletableFuture> { + return client?.getBuildArguments() ?: CompletableFuture.completedFuture(emptyList()) + } + + override fun checkGradleWrapperAvailability(): CompletableFuture { + return client?.checkGradleWrapperAvailability() + ?: CompletableFuture.completedFuture(GradleWrapperCheckResult(false)) + } +}