Implemented logic to run tasks using the Tooling API

This commit is contained in:
Akash Yadav
2022-04-30 11:56:47 +05:30
parent 9819e20a12
commit a76552c8ee
22 changed files with 387 additions and 302 deletions
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="b36e6c44" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-04-30T06:17:07.089058600Z" />
</component>
</project>
Binary file not shown.
@@ -85,7 +85,6 @@ import com.itsaky.androidide.fragments.SearchResultFragment;
import com.itsaky.androidide.fragments.sheets.OptionsListFragment;
import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.fragments.sheets.TextSheetFragment;
import com.itsaky.androidide.handlers.BuildServiceHandler;
import com.itsaky.androidide.handlers.EditorEventListener;
import com.itsaky.androidide.handlers.FileOptionsHandler;
import com.itsaky.androidide.handlers.IDEHandler;
@@ -94,6 +93,7 @@ import com.itsaky.androidide.interfaces.EditorActivityProvider;
import com.itsaky.androidide.lsp.IDELanguageClientImpl;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.managers.ToolsManager;
import com.itsaky.androidide.models.ApkMetadata;
import com.itsaky.androidide.models.DiagnosticGroup;
import com.itsaky.androidide.models.LogLine;
import com.itsaky.androidide.models.SaveResult;
@@ -103,9 +103,9 @@ import com.itsaky.androidide.project.AndroidProject;
import com.itsaky.androidide.project.IDEProject;
import com.itsaky.androidide.services.GradleBuildService;
import com.itsaky.androidide.services.LogReceiver;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.shell.ShellServer;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult;
import com.itsaky.androidide.tooling.api.model.IdeGradleProject;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.EditorActivityActions;
@@ -142,6 +142,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -174,7 +175,6 @@ public class EditorActivity extends StudioActivity
private FileTreeFragment mFileTreeFragment;
private TreeNode mLastHeld;
private SymbolInputView symbolInput;
private BuildServiceHandler mBuildServiceHandler;
private FileOptionsHandler mFileOptionsHandler;
private QuickAction mTabCloseAction;
private TextSheetFragment mDaemonStatusFragment;
@@ -184,6 +184,8 @@ public class EditorActivity extends StudioActivity
private ActivityResultLauncher<Intent> mUIDesignerLauncher;
private EditorBottomSheetBehavior<? extends View> mEditorBottomSheet;
private EditorViewModel mViewModel;
private IdeGradleProject mRootProject;
private GradleBuildService mBuildService;
private final ServiceConnection mGradleServiceConnection =
new ServiceConnection() {
@@ -324,14 +326,6 @@ public class EditorActivity extends StudioActivity
}
}
public void setStatus(final CharSequence text) {
try {
runOnUiThread(() -> mBinding.bottomSheet.statusText.setText(text));
} catch (Throwable th) {
LOG.error("Failed to update status text", th);
}
}
public void handleSearchResults(Map<File, List<SearchResult>> results) {
setSearchResultAdapter(
new com.itsaky.androidide.adapters.SearchListAdapter(
@@ -701,20 +695,6 @@ public class EditorActivity extends StudioActivity
return saveAll(true);
}
public void install(@NonNull File apk) {
LOG.debug("Installing APK:", apk);
if (apk.exists()) {
Intent i = IntentUtils.getInstallAppIntent(apk);
if (i != null) {
startActivity(i);
} else {
getApp().toast(R.string.msg_apk_install_intent_failed, Toaster.Type.ERROR);
}
} else {
LOG.error("APK file does not exist!");
}
}
public void updateServices() {
new TaskExecutor()
.executeAsync(
@@ -732,6 +712,14 @@ public class EditorActivity extends StudioActivity
__ -> setStatus(getString(R.string.msg_service_started)));
}
public void setStatus(final CharSequence text) {
try {
runOnUiThread(() -> mBinding.bottomSheet.statusText.setText(text));
} catch (Throwable th) {
LOG.error("Failed to update status text", th);
}
}
public void closeFile(int index) {
if (index >= 0 && index < mViewModel.getOpenedFileCount()) {
var opened = mViewModel.getOpenedFile(index);
@@ -894,9 +882,8 @@ public class EditorActivity extends StudioActivity
notifySyncNeeded();
}
if (result.xmlSaved && canProcessResources && getBuildService() != null) {
getBuildService().updateResourceClasses();
}
// TODO Notify language servers about changed XML files
// Maybe use a file watcher to do these kind of things.
return result.gradleSaved;
}
@@ -911,19 +898,14 @@ public class EditorActivity extends StudioActivity
}
private void notifySyncNeeded() {
if (getBuildService() != null && !getBuildService().isBuilding()) {
if (mBuildService != null && !mBuildService.isBuildInProgress()) {
getSyncBanner()
.setNegative(android.R.string.cancel, null)
.setPositive(
android.R.string.ok, v -> getBuildServiceHandler().assembleDebug(false))
.setPositive(android.R.string.ok, v -> assembleDebug(false))
.show();
}
}
private IDEService getBuildService() {
return mBuildServiceHandler.getService();
}
public void saveResult(int index, SaveResult result) {
if (index >= 0 && index < mViewModel.getOpenedFileCount()) {
var frag = getEditorAtIndex(index);
@@ -971,8 +953,116 @@ public class EditorActivity extends StudioActivity
.setContentText(R.string.msg_sync_needed);
}
public BuildServiceHandler getBuildServiceHandler() {
return mBuildServiceHandler;
public void assembleDebug(boolean installApk) {
execTasks(installApk ? installableTaskResultConsumer("debug") : null, "assembleDebug");
}
@NonNull
public CompletableFuture<TaskExecutionResult> execTasks(
Consumer<TaskExecutionResult> resultHandler, String... tasks) {
return mBuildService
.executeTasks(tasks)
.whenComplete(
((executionResult, throwable) -> {
if (executionResult == null || throwable != null) {
LOG.error("Tasks failed to execute", TextUtils.join(", ", tasks));
}
if (resultHandler != null) {
resultHandler.accept(executionResult);
}
}));
}
public Consumer<TaskExecutionResult> installableTaskResultConsumer(
@NonNull String variantName) {
return task -> {
if (task != null) {
// TODO Handle multiple application modules
final var app = mRootProject.findFirstApplicationModule();
if (app == null) {
LOG.error("No application module found in root project. Cannot install APKs");
return;
}
final var variants = app.getVariants();
final var foundVariant =
variants.stream()
.filter(variant -> variant.getName().equals(variantName))
.findFirst();
if (foundVariant.isPresent()) {
final var variant = foundVariant.get();
final var main = variant.getMainArtifact();
final var outputListingFile = main.getAssembleTaskOutputListingFile();
if (outputListingFile == null) {
LOG.error("No output listing file provided with project model");
return;
}
final var apkFile = ApkMetadata.findApkFile(outputListingFile);
if (apkFile == null) {
LOG.error(
"No apk file specified in output listing file:", outputListingFile);
return;
}
install(apkFile);
} else {
LOG.error(
"No",
variantName,
"variant found in application module",
app.getProjectPath());
}
}
};
}
public void install(@NonNull File apk) {
LOG.debug("Installing APK:", apk);
if (apk.exists()) {
Intent i = IntentUtils.getInstallAppIntent(apk);
if (i != null) {
startActivity(i);
} else {
getApp().toast(R.string.msg_apk_install_intent_failed, Toaster.Type.ERROR);
}
} else {
LOG.error("APK file does not exist!");
}
}
public void assembleRelease() {
execTasks(null, "assembleRelease");
}
public void build() {
execTasks(null, "build");
}
public void clean() {
execTasks(null, "clean");
}
public void bundle() {
execTasks(null, "bundle");
}
public void lint() {
execTasks(null, "lint");
}
public void lintDebug() {
execTasks(null, "lintDebug");
}
public void lintRelease() {
execTasks(null, "lintRelease");
}
public void cleanAndRebuild() {
execTasks(null, "clean", "build");
}
/////////////////////////////////////////////////
@@ -990,6 +1080,10 @@ public class EditorActivity extends StudioActivity
}
}
public GradleBuildService getBuildService() {
return mBuildService;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -1013,7 +1107,6 @@ public class EditorActivity extends StudioActivity
createQuickActions();
mBuildEventListener.setActivity(this);
mBuildServiceHandler = new BuildServiceHandler(this);
mFileOptionsHandler = new FileOptionsHandler(this);
startServices();
@@ -1181,6 +1274,8 @@ public class EditorActivity extends StudioActivity
}
private void initializeProject() {
// TODO Do not create unnecessary models. Instead, just keep a reference to the root
// directory of the project.
final var androidProject = getAndroidProject();
if (androidProject == null) {
LOG.error("Cannot initialize project. Project model is null.");
@@ -1212,6 +1307,7 @@ public class EditorActivity extends StudioActivity
}
protected void onProjectInitialized(IdeGradleProject root) {
mRootProject = root;
CompletableFuture.runAsync(
() -> {
final var app = mBuildService.findFirstAndroidModule(root);
@@ -1487,8 +1583,6 @@ public class EditorActivity extends StudioActivity
// Because it would work anyway
// But still we do...
mFileOptionsHandler.start();
mBuildServiceHandler.start();
// getBuildServiceHandler().assembleDebug(false);
}
private void initializeLanguageServers() {
@@ -1514,14 +1608,7 @@ public class EditorActivity extends StudioActivity
shutdownLanguageServers();
if (getBuildService() != null) {
getBuildService().setListener(null);
if (mBuildServiceHandler != null) {
mBuildServiceHandler.stop();
} else {
getBuildService().exit();
}
}
// TODO Send shutdown request to Tooling API server
if (mFileOptionsHandler != null) {
mFileOptionsHandler.stop();
@@ -42,7 +42,7 @@ abstract class BaseBuildAction : EditorActivityAction() {
enabled = true
}
}
override fun postExec(data: ActionData, result: Any) {
val context = getActivity(data) ?: return
context.invalidateOptionsMenu()
@@ -51,5 +51,5 @@ abstract class BaseBuildAction : EditorActivityAction() {
fun shouldPrepare() = visible && enabled
private fun isBuildInProgress(activity: EditorActivity): Boolean =
activity.buildServiceHandler.isBuilding
activity.buildService == null || activity.buildService.isBuildInProgress
}
@@ -36,7 +36,7 @@ class AssembleDebugAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val activity = getActivity(data) ?: return false
activity.buildServiceHandler.assembleDebug(true)
activity.assembleDebug(true)
return true
}
}
@@ -36,7 +36,7 @@ class AssembleReleaseAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val activity = getActivity(data) ?: return false
activity.buildServiceHandler.service.assembleRelease()
activity.assembleRelease()
return true
}
}
@@ -36,7 +36,7 @@ class BuildAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val activity = getActivity(data) ?: return false
activity.buildServiceHandler.service.build()
activity.build()
return true
}
}
@@ -35,7 +35,7 @@ class CleanAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.clean()
context.clean()
return true
}
}
@@ -35,7 +35,7 @@ class CreateAABAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.bundle()
context.bundle()
return true
}
}
@@ -39,7 +39,7 @@ class QuickRunAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.assembleDebug(true)
context.assembleDebug(true)
return true
}
}
@@ -35,7 +35,7 @@ class RebuildAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.cleanAndRebuild()
context.cleanAndRebuild()
return true
}
}
@@ -40,8 +40,7 @@ class StopDaemonsAction() : EditorActivityAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.stopAllDaemons()
context.app.stopAllDaemons()
return true
}
}
@@ -35,7 +35,7 @@ class LintAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.lint()
context.lint()
return true
}
}
@@ -35,7 +35,7 @@ class LintDebugAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.lintDebug()
context.lintDebug()
return true
}
}
@@ -35,7 +35,7 @@ class LintReleaseAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.lintRelease()
context.lintRelease()
return true
}
}
@@ -1,222 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.handlers;
import android.view.View;
import com.itsaky.androidide.R;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.project.AndroidProject;
import com.itsaky.androidide.project.IDEModule;
import com.itsaky.androidide.project.IDEProject;
import com.itsaky.androidide.services.builder.BuildListener;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.GradleTask;
import com.itsaky.androidide.tasks.gradle.build.ApkGeneratingTask;
import com.itsaky.androidide.utils.Environment;
import java.io.File;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
public class BuildServiceHandler extends IDEHandler implements BuildListener {
private IDEService service;
private boolean shouldInstallApk = false;
public BuildServiceHandler(Provider provider) {
super(provider);
}
public boolean isBuilding() {
return getService().isBuilding();
}
public IDEService getService() {
return service;
}
@Override
public void start() {
Objects.requireNonNull(this.activity());
AndroidProject project = provider.provideAndroidProject();
Objects.requireNonNull(project);
Objects.requireNonNull(project.getProjectPath());
this.service = new IDEService(new File(project.getProjectPath()));
this.service.setListener(this);
}
@Override
public void stop() {
this.service.stopAllDaemons();
this.service.exit();
}
@Override
public void onBuildModified() {
// Does nothing currently
}
@Override
public void onProjectLoaded(IDEProject project, Optional<IDEModule> appModule) {
project.iconPath = provider.provideAndroidProject().getIconPath();
activity().setIDEProject(project);
if (appModule.isPresent()) {
IDEModule app = appModule.get();
activity().setStatus(activity().getString(R.string.msg_starting_completion));
final List<String> paths =
app.dependencies.stream()
.filter(this::isClasspathValid)
.collect(Collectors.toList());
provider.provideAndroidProject().setClassPaths(paths);
File androidJar =
new File(
Environment.ANDROID_HOME,
String.format("platforms/%s/android.jar", app.compileSdkVersion));
if (androidJar.exists()) {
Environment.setBootClasspath(androidJar);
provider.provideAndroidProject().addClasspath(androidJar.getAbsolutePath());
activity().updateServices();
} else {
activity().setStatus("android.jar not found!");
}
} else {
activity().setStatus("Cannot get :app module...");
}
}
@Override
public void onStartingGradleDaemon(GradleTask task) {
activity().setStatus(activity().getString(R.string.msg_starting_daemon));
}
@Override
public void onRunTask(GradleTask task, String name) {
if (task != null && task.canOutput()) {
activity().setStatus(service.typeString(task.getTaskID()) + " " + name);
}
}
@Override
public void onBuildSuccessful(GradleTask task, String msg) {
appendOutput(task, msg);
analyzeCurrentFile();
if (task == null || activity() == null) {
return;
}
if (task.canOutput()) {
activity().setStatus(msg);
}
// If this task generates APK, ask user if he/she wants to install them
if (shouldInstallApk && task instanceof ApkGeneratingTask) {
final IDEProject project = activity().provideIDEProject();
final Optional<IDEModule> app =
project.getModuleByPath(":app"); // TODO Handle multiple application modules
if (app.isPresent()) {
final String path = app.get().projectDir;
if (path != null) {
final File buildDir = new File(path, "build");
// installApks(((ApkGeneratingTask) task).getApks(buildDir));
}
}
}
activity()
.getApp()
.getPrefManager()
.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false);
activity().getBinding().buildProgressIndicator.setVisibility(View.GONE);
}
@Override
public void onBuildFailed(GradleTask task, String msg) {
appendOutput(task, msg);
analyzeCurrentFile();
if (task == null) {
return;
}
if (task.canOutput()) {
activity().setStatus(msg);
}
activity()
.getApp()
.getPrefManager()
.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false);
activity().getBinding().buildProgressIndicator.setVisibility(View.GONE);
}
@Override
public void saveFiles() {
activity().saveAll(false, false);
}
@Override
public void appendOutput(GradleTask task, CharSequence text) {
if (text == null) {
return;
}
if (task != null && !task.canOutput()) {
return;
}
activity().appendBuildOut(text.toString());
}
@Override
public void prepareBuild() {}
private void analyzeCurrentFile() {
final var editorView = activity().getCurrentEditor();
if (editorView != null) {
final var editor = editorView.getEditor();
if (editor != null) {
editor.analyze();
}
}
}
private boolean isClasspathValid(String path) {
if (path == null || path.trim().length() <= 0 || path.trim().equals("/")) {
return false;
}
File file = new File(path);
return file.isFile()
&& file.getName().endsWith(".jar")
// Release R.jar shouldn't be included in classpath
&& !file.getAbsolutePath().endsWith("/release/R.jar");
}
public void assembleDebug(boolean shouldInstallApk) {
setShouldInstallApk(shouldInstallApk);
getService().assembleDebug();
}
public void setShouldInstallApk(boolean install) {
this.shouldInstallApk = install;
}
}
@@ -56,15 +56,40 @@ class EditorEventListener : GradleBuildService.EventListener {
activity().binding.buildProgressIndicator.visibility = View.VISIBLE
}
override fun onBuildSuccessful(tasks: MutableList<String>) {
analyzeCurrentFile()
activity().app.prefManager.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false)
activity().binding.buildProgressIndicator.visibility = View.GONE
// TODO If the tasks generate APKs, start the APK install intent
}
override fun onBuildFailed(tasks: MutableList<String>) {
analyzeCurrentFile()
activity().app.prefManager.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false)
activity().binding.buildProgressIndicator.visibility = View.GONE
}
override fun onOutput(line: String?) {
activity().appendBuildOut(line)
// TODO This can be handled better when ProgressEvents are received from Tooling API server
if (line!!.startsWith("> Task")) {
if (line!!.startsWith("> Task") ||
line.contains("BUILD SUCCESSFUL") ||
line.contains("BUILD FAILED")) {
activity().setStatus(line)
}
}
private fun analyzeCurrentFile() {
val editorView = activity().currentEditor
if (editorView != null) {
val editor = editorView.editor
editor?.analyze()
}
}
fun activity(): EditorActivity {
return activityReference.get()
?: throw IllegalStateException("Activity reference has been destroyed!")
@@ -17,15 +17,25 @@
*/
package com.itsaky.androidide.models;
import static com.itsaky.androidide.utils.ILogger.newInstance;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.JSONUtility;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.List;
@SuppressWarnings("unused")
public class ApkMetadata {
private static final ILogger LOG = newInstance("ApkMetadata");
@SerializedName("version")
private int version;
@@ -44,12 +54,84 @@ public class ApkMetadata {
@SerializedName("elementType")
private String elementType;
public int getVersion() {
return version;
@Nullable
public static File findApkFile(@NonNull File metadataFile) {
try {
final var dir = metadataFile.getParentFile();
final var metadata =
JSONUtility.gson.fromJson(new FileReader(metadataFile), ApkMetadata.class);
if (!isValid(metadata)) {
LOG.warn("Invalid APK metadata:", metadata);
return null;
}
for (Element element : metadata.getElements()) {
if (element == null || element.getOutputFile() == null) {
LOG.warn("No output file specified in APK metadata element:", element);
continue;
}
if (element.getOutputFile().endsWith(".apk")) {
final File apk = new File(dir, element.getOutputFile());
if (apk.exists() && apk.isFile()) {
LOG.info("Found apk in metadata:", apk);
return apk;
}
}
}
return null;
} catch (FileNotFoundException e) {
LOG.error("Metadata file not found...", e);
return null;
}
}
public void setVersion(int version) {
this.version = version;
private static boolean isValid(ApkMetadata metadata) {
// Null checks
if (metadata == null
|| metadata.getArtifactType() == null
|| metadata.getArtifactType().getType() == null
|| metadata.getElements() == null) {
LOG.warn("APK metadata null check failed. Metadata:", metadata);
return false;
}
final ArtifactType type = metadata.getArtifactType();
final List<Element> elements = metadata.getElements();
if (!type.getType().equals(ArtifactType.TYPE_APK)) {
LOG.warn("Artifact is not of type APK. Metadata:", metadata);
return false;
}
if (elements.isEmpty()) {
LOG.warn("No output elements found for metadata:", metadata);
return false;
}
boolean atLeastOneApk = false;
for (Element element : elements) {
if (element == null
|| element.getOutputFile() == null
|| !element.getOutputFile().endsWith(".apk")) {
LOG.warn("Skipping output element because file is not APK:", element);
continue;
}
if (element.getOutputFile().endsWith(".apk")) {
atLeastOneApk = true;
break;
}
}
LOG.debug("Output metadata validation succeeded");
return atLeastOneApk;
}
public List<Element> getElements() {
return elements;
}
public ArtifactType getArtifactType() {
@@ -60,6 +142,18 @@ public class ApkMetadata {
this.artifactType = artifactType;
}
public void setElements(List<Element> elements) {
this.elements = elements;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
public String getApplicationId() {
return applicationId;
}
@@ -76,14 +170,6 @@ public class ApkMetadata {
this.variantName = variantName;
}
public List<Element> getElements() {
return elements;
}
public void setElements(List<Element> elements) {
this.elements = elements;
}
public String getElementType() {
return elementType;
}
@@ -56,6 +56,7 @@ import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
@@ -72,6 +73,7 @@ public class GradleBuildService extends Service implements BuildService, IToolin
private final ILogger SERVER_LOGGER = newInstance("ToolingApiServer");
private final IBinder mBinder = new GradleServiceBinder();
private boolean isToolingServerStarted = false;
private boolean isBuildInProgress = false;
private Thread toolingServerThread;
private NotificationManager notificationManager;
private IToolingApiServer server;
@@ -145,12 +147,34 @@ public class GradleBuildService extends Service implements BuildService, IToolin
}
}
@Override
public void onBuildSuccessful(@NonNull List<String> tasks) {
if (eventListener != null) {
eventListener.onBuildSuccessful(tasks);
}
}
@Override
public void onBuildFailed(@NonNull List<String> tasks) {
if (eventListener != null) {
eventListener.onBuildFailed(tasks);
}
}
@NonNull
@Override
public CompletableFuture<InitializeResult> initializeProject(@NonNull String rootDir) {
checkServerStarted();
ensureTmpdir();
return server.initialize(new InitializeProjectMessage(rootDir));
if (isBuildInProgress) {
logBuildInProgress();
return failedFutureForBuildInProgress();
}
isBuildInProgress = true;
return server.initialize(new InitializeProjectMessage(rootDir))
.thenApply(this::markBuildAsFinished);
}
@NonNull
@@ -158,7 +182,15 @@ public class GradleBuildService extends Service implements BuildService, IToolin
public CompletableFuture<TaskExecutionResult> executeTasks(@NonNull String... tasks) {
checkServerStarted();
ensureTmpdir();
return server.executeTasks(new TaskExecutionMessage(":", Arrays.asList(tasks)));
if (isBuildInProgress) {
logBuildInProgress();
return failedFutureForBuildInProgress();
}
isBuildInProgress = true;
return server.executeTasks(new TaskExecutionMessage(":", Arrays.asList(tasks)))
.thenApply(this::markBuildAsFinished);
}
@NonNull
@@ -167,7 +199,15 @@ public class GradleBuildService extends Service implements BuildService, IToolin
@NonNull String projectPath, @NonNull String... tasks) {
checkServerStarted();
ensureTmpdir();
return server.executeTasks(new TaskExecutionMessage(projectPath, Arrays.asList(tasks)));
if (isBuildInProgress) {
logBuildInProgress();
return failedFutureForBuildInProgress();
}
isBuildInProgress = true;
return server.executeTasks(new TaskExecutionMessage(projectPath, Arrays.asList(tasks)))
.thenApply(this::markBuildAsFinished);
}
private void checkServerStarted() {
@@ -180,6 +220,27 @@ public class GradleBuildService extends Service implements BuildService, IToolin
Environment.mkdirIfNotExits(Environment.TMP_DIR);
}
private void logBuildInProgress() {
LOG.warn("A build is already in progress!");
}
@NonNull
private <T> CompletableFuture<T> failedFutureForBuildInProgress() {
final var future = new CompletableFuture<T>();
future.completeExceptionally(
new CompletionException(new IllegalStateException("A build is already running!")));
return future;
}
protected <T> T markBuildAsFinished(T t) {
isBuildInProgress = false;
return t;
}
public boolean isBuildInProgress() {
return isBuildInProgress;
}
/**
* Find the first direct subproject of the given root project that is an {@link
* IdeAndroidModule}.
@@ -303,7 +364,7 @@ public class GradleBuildService extends Service implements BuildService, IToolin
void onServerStarted();
}
/** Handles output received from a Gradle build. */
/** Handles events received from a Gradle build. */
public interface EventListener {
/**
@@ -313,6 +374,22 @@ public class GradleBuildService extends Service implements BuildService, IToolin
*/
void prepareBuild();
/**
* Called when a build is successful.
*
* @param tasks The tasks that were run.
* @see IToolingApiClient#onBuildSuccessful(List)
*/
void onBuildSuccessful(@NonNull List<String> tasks);
/**
* Called when a build fails.
*
* @param tasks The tasks that were run.
* @see IToolingApiClient#onBuildFailed(List)
*/
void onBuildFailed(@NonNull List<String> tasks);
/**
* Called when the output line is received.
*
@@ -61,7 +61,7 @@ public abstract class ApkGeneratingTask extends BaseGradleTask {
LOG.error("No apk directories found. buildDir=" + buildDir);
return result;
}
for (File dir : dirs) {
final File jsonData = new File(dir, "output-metadata.json");
if (!jsonData.exists() || !FileUtils.isUtf8(jsonData)) {
@@ -45,4 +45,20 @@ interface IToolingApiClient {
/** Called just before a build is started. */
@JsonNotification fun prepareBuild()
/**
* Called when a build is successful.
*
* @param tasks The tasks that were run. Maybe an empty list if no tasks were specified or if
* the build was not related to any tasks.
*/
@JsonNotification fun onBuildSuccessful(tasks: List<String>)
/**
* Called when a build fails.
*
* @param tasks The tasks that were run. Maybe an empty list if no tasks were specified or if
* the build was not related to any tasks.
*/
@JsonNotification fun onBuildFailed(tasks: List<String>)
}