Removed unneccessary legacy project and task models

This commit is contained in:
Akash Yadav
2022-05-02 20:19:55 +05:30
parent f2f3fc725f
commit 5c547229d4
42 changed files with 454 additions and 3233 deletions
@@ -99,8 +99,6 @@ import com.itsaky.androidide.models.LogLine;
import com.itsaky.androidide.models.SaveResult;
import com.itsaky.androidide.models.SearchResult;
import com.itsaky.androidide.models.SheetOption;
import com.itsaky.androidide.projects.AndroidProject;
import com.itsaky.androidide.projects.IDEProject;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.services.GradleBuildService;
import com.itsaky.androidide.services.LogReceiver;
@@ -159,7 +157,7 @@ public class EditorActivity extends StudioActivity
EditorActivityProvider,
OptionsListFragment.OnOptionsClickListener {
public static final String EXTRA_PROJECT = "project";
public static final String EXTRA_PROJECT_PATH = "project_path";
public static final String KEY_BOTTOM_SHEET_SHOWN = "editor_bottomSheetShown";
private static final String TAG_FILE_OPTIONS_FRAGMENT = "file_options_fragment";
private static final int ACTION_ID_CLOSE = 100;
@@ -283,34 +281,6 @@ public class EditorActivity extends StudioActivity
return this;
}
@Override
public AndroidProject provideAndroidProject() {
return getAndroidProject();
}
@Override
public IDEProject provideIDEProject() {
return getIDEProject();
}
@Nullable
private IDEProject getIDEProject() {
if (mViewModel == null) {
return null;
}
return mViewModel.getIDEProject();
}
@Nullable
private AndroidProject getAndroidProject() {
if (mViewModel == null) {
return null;
}
return mViewModel.getAndroidProject();
}
public void handleSearchResults(Map<File, List<SearchResult>> results) {
setSearchResultAdapter(
new com.itsaky.androidide.adapters.SearchListAdapter(
@@ -344,7 +314,7 @@ public class EditorActivity extends StudioActivity
shell.bgAppend(
String.format(
"cd '%s' && sh gradlew --status",
Objects.requireNonNull(getAndroidProject()).getProjectPath()));
Objects.requireNonNull(ProjectManager.INSTANCE.getProjectDir())));
if (!getDaemonStatusFragment().isShowing()) {
getDaemonStatusFragment().show(getSupportFragmentManager(), "daemon_status");
}
@@ -808,7 +778,8 @@ public class EditorActivity extends StudioActivity
intent.putExtra(
DesignerActivity.KEY_LAYOUT_PATH,
getCurrentEditor().getFile().getAbsolutePath());
intent.putStringArrayListExtra(DesignerActivity.KEY_RES_DIRS, getResourceDirectories());
intent.putStringArrayListExtra(
DesignerActivity.KEY_RES_DIRS, (ArrayList<String>) getResourceDirectories());
LOG.info("Launching UI Designer...");
mUIDesignerLauncher.launch(intent);
} catch (Throwable th) {
@@ -822,21 +793,13 @@ public class EditorActivity extends StudioActivity
}
@NonNull
private ArrayList<String> getResourceDirectories() {
final var dirs = new ArrayList<String>();
if (getAndroidProject() != null
&& getAndroidProject().getModulePaths() != null
&& !getAndroidProject().getModulePaths().isEmpty()) {
for (String path : getAndroidProject().getModulePaths()) {
if (path != null && new File(path).exists()) {
File res = new File(path, "src/main/res");
if (res.exists()) {
dirs.add(res.getAbsolutePath());
}
}
}
private List<String> getResourceDirectories() {
final var dirs = ProjectManager.INSTANCE.getApplicationResDirectories();
if (dirs == null) {
return Collections.emptyList();
}
return dirs;
return dirs.stream().map(File::getAbsolutePath).collect(Collectors.toList());
}
public boolean saveAll(boolean notify, boolean canProcessResources) {
@@ -1049,10 +1012,6 @@ public class EditorActivity extends StudioActivity
execTasks(null, "clean", "build");
}
/////////////////////////////////////////////////
////////////// PRIVATE APIS /////////////////////
/////////////////////////////////////////////////
public AlertDialog getFindInProjectDialog() {
return mFindInProjectDialog == null ? createFindInProjectDialog() : mFindInProjectDialog;
}
@@ -1064,6 +1023,10 @@ public class EditorActivity extends StudioActivity
}
}
/////////////////////////////////////////////////
////////////// PRIVATE APIS /////////////////////
/////////////////////////////////////////////////
public GradleBuildService getBuildService() {
return mBuildService;
}
@@ -1076,7 +1039,7 @@ public class EditorActivity extends StudioActivity
mViewModel = new ViewModelProvider(this).get(EditorViewModel.class);
getProjectFromIntent();
mFileTreeFragment = FileTreeFragment.newInstance(this.getAndroidProject());
mFileTreeFragment = FileTreeFragment.newInstance();
mDaemonStatusFragment = new TextSheetFragment().setTextSelectable(true);
setupDrawerToggle();
@@ -1260,13 +1223,13 @@ 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) {
final var projectPath = ProjectManager.INSTANCE.getProjectDir();
if (projectPath == null) {
LOG.error("Cannot initialize project. Project model is null.");
return;
}
final var projectDir = new File(androidProject.getProjectPath());
final var projectDir = new File(projectPath);
if (!projectDir.exists()) {
LOG.error("Project directory does not exist. Cannot initialize project");
return;
@@ -1462,25 +1425,21 @@ public class EditorActivity extends StudioActivity
final Intent intent = new Intent(this, TerminalActivity.class);
intent.putExtra(
TerminalActivity.KEY_WORKING_DIRECTORY,
Objects.requireNonNull(getAndroidProject()).getProjectPath());
Objects.requireNonNull(ProjectManager.INSTANCE.getProjectDir()));
startActivity(intent);
}
private void getProjectFromIntent() {
final var project = (AndroidProject) getIntent().getParcelableExtra(EXTRA_PROJECT);
if (mViewModel != null) {
mViewModel.setAndroidProject(project);
} else {
LOG.error("ViewModel is null. Cannot set project.");
}
final var project = getIntent().getStringExtra(EXTRA_PROJECT_PATH);
ProjectManager.INSTANCE.setProjectPath(project);
getApp().getPrefManager()
.setOpenedProject(
Objects.requireNonNull(this.getAndroidProject()).getProjectPath());
.setOpenedProject(Objects.requireNonNull(ProjectManager.INSTANCE.getProjectDir()));
try {
//noinspection ConstantConditions
getSupportActionBar()
.setSubtitle(new File(getAndroidProject().getProjectPath()).getName());
.setSubtitle(new File(ProjectManager.INSTANCE.getProjectDir()).getName());
} catch (Throwable th) {
// ignored
}
@@ -1536,7 +1495,7 @@ public class EditorActivity extends StudioActivity
final var javaLanguageServer = getApp().getJavaLanguageServer();
final var workspaceRoots = new HashSet<Path>();
workspaceRoots.add(
new File(Objects.requireNonNull(getAndroidProject()).getProjectPath()).toPath());
new File(Objects.requireNonNull(ProjectManager.INSTANCE.getProjectDir())).toPath());
workspaceRoots.add(Environment.HOME.toPath().resolve("logsender"));
final var params = new InitializeParams(workspaceRoots);
@@ -1607,28 +1566,35 @@ public class EditorActivity extends StudioActivity
builder.show();
}
@Nullable
@NonNull
private AlertDialog createFindInProjectDialog() {
if (getAndroidProject() == null
|| getAndroidProject().getModulePaths() == null
|| getAndroidProject().getModulePaths().size() <= 0) {
getApp().toast(R.string.msg_no_modules, Toaster.Type.ERROR);
return null;
List<File> moduleDirs = null;
final var rootProject = ProjectManager.INSTANCE.getRootProject();
if (rootProject == null) {
LOG.warn("No root project model found. Is the project initialized?");
moduleDirs = new ArrayList<>(0);
}
final List<String> modules = getAndroidProject().getModulePaths();
if (moduleDirs == null) {
moduleDirs =
rootProject.getModules().stream()
.map(IdeGradleProject::getProjectDir)
.collect(Collectors.toList());
}
final List<File> srcDirs = new ArrayList<>();
final LayoutSearchProjectBinding binding =
LayoutSearchProjectBinding.inflate(getLayoutInflater());
binding.modulesContainer.removeAllViews();
for (int i = 0; i < modules.size(); i++) {
final File file = new File(modules.get(i));
final File src = new File(file, "src");
if (!file.exists() || !file.isDirectory() || !src.exists() || !src.isDirectory()) {
for (int i = 0; i < moduleDirs.size(); i++) {
final File module = moduleDirs.get(i);
final File src = new File(module, "src");
if (!module.exists() || !module.isDirectory() || !src.exists() || !src.isDirectory()) {
continue;
}
CheckBox check = new CheckBox(this);
check.setText(file.getName());
check.setText(module.getName());
check.setChecked(true);
LinearLayout.MarginLayoutParams params = new LinearLayout.MarginLayoutParams(-2, -2);
@@ -39,18 +39,15 @@ import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.models.NewProjectDetails;
import com.itsaky.androidide.models.ProjectTemplate;
import com.itsaky.androidide.projects.AndroidProject;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.callables.ProjectCreatorCallable;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.ProjectFinder;
import com.itsaky.androidide.utils.TransformUtils;
import com.itsaky.toaster.Toaster;
import java.io.File;
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.Callable;
import abhishekti7.unicorn.filepicker.UnicornFilePicker;
@@ -68,11 +65,213 @@ public class MainActivity extends StudioActivity
private int currentTemplateIndex = 0;
@Override
protected View bindLayout() {
binding = ActivityMainBinding.inflate(getLayoutInflater());
createBinding = binding.createLayout;
createLayoutBinding = createBinding.createProjectContent;
return binding.getRoot();
public void onClick(View p1) {
if (p1.getId() == binding.createProject.getId()) {
showCreateProject();
} else if (p1.getId() == createBinding.createprojectClose.getId()) {
hideCreateProject();
} else if (p1.getId() == createLayoutBinding.nextCard.getId()) {
currentTemplateIndex++;
if (currentTemplateIndex >= mTemplates.size()) {
currentTemplateIndex = 0;
}
showTemplate(currentTemplateIndex);
} else if (p1.getId() == createLayoutBinding.previousCard.getId()) {
currentTemplateIndex--;
if (currentTemplateIndex < 0) {
currentTemplateIndex = mTemplates.size() - 1;
}
showTemplate(currentTemplateIndex);
} else if (p1.getId() == createLayoutBinding.createprojectCreate.getId()) {
createNewProject();
} else if (p1.getId() == binding.gotoPreferences.getId()) {
gotoSettings();
} else if (p1.getId() == binding.openProject.getId()) {
pickProject();
} else if (p1.getId() == binding.openTerminal.getId()) {
startActivity(new Intent(this, TerminalActivity.class));
}
}
private void showCreateProject() {
MaterialContainerTransform transform =
TransformUtils.createContainerTransformFor(
binding.createProject, binding.createNewCard, binding.realContainer);
TransitionManager.beginDelayedTransition(binding.getRoot(), transform);
binding.createNewCard.setVisibility(View.VISIBLE);
}
private void hideCreateProject() {
MaterialContainerTransform transform =
TransformUtils.createContainerTransformFor(
binding.createNewCard, binding.createProject, binding.realContainer);
TransitionManager.beginDelayedTransition(binding.getRoot(), transform);
binding.createNewCard.setVisibility(View.GONE);
}
private void showTemplate(int index) {
if (index < 0 || index >= mTemplates.size()) {
return;
}
final ProjectTemplate template = mTemplates.get(index);
createLayoutBinding.createprojectTemplateLabel.setText(template.getName());
createLayoutBinding.createprojectTemplateDescription.setText(template.getDescription());
createLayoutBinding.createprojectTemplateImage.setImageResource(template.getImageId());
}
private void createNewProject() {
final String appName =
Objects.requireNonNull(createLayoutBinding.createprojectTextAppName.getEditText())
.getText()
.toString()
.trim();
final String packageName =
Objects.requireNonNull(
createLayoutBinding.createprojectTextPackageName.getEditText())
.getText()
.toString()
.trim();
final int minSdk = getMinSdk();
final int targetSdk = getTargetSdk();
if (!isValid(appName, packageName, minSdk, targetSdk)) {
getApp().toast(R.string.invalid_values, Toaster.Type.ERROR);
return;
}
createProject(appName, packageName, minSdk, targetSdk);
}
private void gotoSettings() {
startActivity(new Intent(this, PreferencesActivity.class));
}
private void pickProject() {
UnicornFilePicker.from(this)
.addConfigBuilder()
.addItemDivider(false)
.selectMultipleFiles(false)
.setRootDirectory(Environment.getExternalStorageDirectory().getAbsolutePath())
.showHiddenFiles(true)
.showOnlyDirectory(true)
.theme(R.style.AppTheme_FilePicker)
.build()
.forResult(abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE);
}
private int getMinSdk() {
try {
return Integer.parseInt(
Objects.requireNonNull(
createLayoutBinding.createprojectTextMinSdk.getEditText())
.getText()
.toString());
} catch (Exception e) {
getApp().toast(e.getMessage(), Toaster.Type.ERROR);
}
return -1;
}
private int getTargetSdk() {
try {
return Integer.parseInt(
Objects.requireNonNull(
createLayoutBinding.createprojectTextTargetSdk.getEditText())
.getText()
.toString());
} catch (Exception e) {
getApp().toast(e.getMessage(), Toaster.Type.ERROR);
}
return -1;
}
private boolean isValid(String appName, String packageName, int minSdk, int targetSdk) {
return appName != null
&& appName.length() > 0
&& packageName != null
&& packageName.length() > 0
&& minSdk > 1
&& minSdk < 99
&& targetSdk > 1
&& targetSdk < 99;
}
private void createProject(String appName, String packageName, int minSdk, int targetSdk) {
new TaskExecutor()
.executeAsync(
new ProjectCreatorCallable(
mTemplates.get(currentTemplateIndex),
new NewProjectDetails(appName, packageName, minSdk, targetSdk),
this),
r -> {});
}
@Override
public void beforeBegin() {
if (mProgressSheet == null) {
createProgressSheet();
}
setMessage(R.string.msg_begin_project_write);
}
@Override
public void onProcessTask(String taskName) {
setMessage(taskName);
}
@Override
public void onSuccess(File root) {
if (mProgressSheet == null) {
createProgressSheet();
}
if (mProgressSheet.isShowing()) {
mProgressSheet.dismiss();
}
getApp().toast(R.string.project_created_successfully, Toaster.Type.SUCCESS);
openProject(root);
}
@Override
public void onFailed(String reason) {
if (mProgressSheet == null) {
createProgressSheet();
}
if (mProgressSheet.isShowing()) {
mProgressSheet.dismiss();
}
getApp().toast(reason, Toaster.Type.ERROR);
}
private void openProject(File root) {
final var intent = new Intent(this, EditorActivity.class);
intent.putExtra(EditorActivity.EXTRA_PROJECT_PATH, root.getAbsolutePath());
startActivity(intent);
}
private void createProgressSheet() {
mProgressSheet = new ProgressSheet();
mProgressSheet.setShowShadow(false);
}
private void setMessage(int msg) {
setMessage(getString(msg));
}
private void setMessage(String msg) {
mProgressSheet.setMessage(msg);
}
@Override
public void onBackPressed() {
if (binding.createNewCard.getVisibility() == View.VISIBLE) {
hideCreateProject();
} else {
super.onBackPressed();
}
}
@Override
@@ -127,20 +326,6 @@ public class MainActivity extends StudioActivity
});
}
private void askProjectOpenPermission(File root) {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(R.string.title_confirm_open_project);
builder.setMessage(getString(R.string.msg_confirm_open_project, root.getAbsolutePath()));
builder.setCancelable(false);
builder.setPositiveButton(R.string.yes, (d, w) -> openProject(root));
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private void gotoSettings() {
startActivity(new Intent(this, PreferencesActivity.class));
}
@Override
protected void onStorageGranted() {}
@@ -151,120 +336,11 @@ public class MainActivity extends StudioActivity
}
@Override
public void onClick(View p1) {
if (p1.getId() == binding.createProject.getId()) {
showCreateProject();
} else if (p1.getId() == createBinding.createprojectClose.getId()) {
hideCreateProject();
} else if (p1.getId() == createLayoutBinding.nextCard.getId()) {
currentTemplateIndex++;
if (currentTemplateIndex >= mTemplates.size()) {
currentTemplateIndex = 0;
}
showTemplate(currentTemplateIndex);
} else if (p1.getId() == createLayoutBinding.previousCard.getId()) {
currentTemplateIndex--;
if (currentTemplateIndex < 0) {
currentTemplateIndex = mTemplates.size() - 1;
}
showTemplate(currentTemplateIndex);
} else if (p1.getId() == createLayoutBinding.createprojectCreate.getId()) {
createNewProject();
} else if (p1.getId() == binding.gotoPreferences.getId()) {
gotoSettings();
} else if (p1.getId() == binding.openProject.getId()) {
pickProject();
} else if (p1.getId() == binding.openTerminal.getId()) {
startActivity(new Intent(this, TerminalActivity.class));
}
}
@Override
public void beforeBegin() {
if (mProgressSheet == null) {
createProgressSheet();
}
setMessage(R.string.msg_begin_project_write);
}
@Override
public void onProcessTask(String taskName) {
setMessage(taskName);
}
@Override
public void onSuccess(File root) {
if (mProgressSheet == null) {
createProgressSheet();
}
if (mProgressSheet.isShowing()) {
mProgressSheet.dismiss();
}
getApp().toast(R.string.project_created_successfully, Toaster.Type.SUCCESS);
openProject(root);
}
@Override
public void onFailed(String reason) {
if (mProgressSheet == null) {
createProgressSheet();
}
if (mProgressSheet.isShowing()) {
mProgressSheet.dismiss();
}
getApp().toast(reason, Toaster.Type.ERROR);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE
&& resultCode == RESULT_OK) {
ArrayList<String> files = data.getStringArrayListExtra("filePaths");
if (files != null) {
if (files.size() == 1) {
File choseDir = new File(files.get(0));
if (choseDir.exists() && choseDir.isDirectory()) {
openProject(choseDir);
} else {
getApp().toast(R.string.msg_picked_isnt_dir, Toaster.Type.ERROR);
}
} else {
getApp().toast(R.string.msg_pick_single_file, Toaster.Type.ERROR);
}
}
}
}
@Override
public void onBackPressed() {
if (binding.createNewCard.getVisibility() == View.VISIBLE) {
hideCreateProject();
} else {
super.onBackPressed();
}
}
@Override
protected void onResume() {
super.onResume();
if (mProgressSheet != null && mProgressSheet.isShowing()) {
mProgressSheet.dismiss();
}
}
private void showTemplate(int index) {
if (index < 0 || index >= mTemplates.size()) {
return;
}
final ProjectTemplate template = mTemplates.get(index);
createLayoutBinding.createprojectTemplateLabel.setText(template.getName());
createLayoutBinding.createprojectTemplateDescription.setText(template.getDescription());
createLayoutBinding.createprojectTemplateImage.setImageResource(template.getImageId());
protected View bindLayout() {
binding = ActivityMainBinding.inflate(getLayoutInflater());
createBinding = binding.createLayout;
createLayoutBinding = createBinding.createProjectContent;
return binding.getRoot();
}
/** Must be called in/after onViewCreated */
@@ -310,147 +386,42 @@ public class MainActivity extends StudioActivity
mTemplates.add(libgdx);
}
private void createProgressSheet() {
mProgressSheet = new ProgressSheet();
mProgressSheet.setShowShadow(false);
private void askProjectOpenPermission(File root) {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(R.string.title_confirm_open_project);
builder.setMessage(getString(R.string.msg_confirm_open_project, root.getAbsolutePath()));
builder.setCancelable(false);
builder.setPositiveButton(R.string.yes, (d, w) -> openProject(root));
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private void setMessage(int msg) {
setMessage(getString(msg));
}
private void setMessage(String msg) {
mProgressSheet.setMessage(msg);
}
private void openProject(File root) {
setMessage(R.string.msg_opening_project);
mProgressSheet.setCancelable(false);
mProgressSheet.show(getSupportFragmentManager(), "opening_project");
new TaskExecutor().executeAsync(new ProjectReader(root), this::openEditor);
}
private void pickProject() {
UnicornFilePicker.from(this)
.addConfigBuilder()
.addItemDivider(false)
.selectMultipleFiles(false)
.setRootDirectory(Environment.getExternalStorageDirectory().getAbsolutePath())
.showHiddenFiles(true)
.showOnlyDirectory(true)
.theme(R.style.AppTheme_FilePicker)
.build()
.forResult(abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE);
}
private void openEditor(AndroidProject project) {
if (project == null) {
getApp().toast(R.string.msg_invalid_project, Toaster.Type.ERROR);
} else {
startActivity(
new Intent(this, EditorActivity.class)
.putExtra(EditorActivity.EXTRA_PROJECT, project));
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE
&& resultCode == RESULT_OK) {
ArrayList<String> files = data.getStringArrayListExtra("filePaths");
if (files != null) {
if (files.size() == 1) {
File choseDir = new File(files.get(0));
if (choseDir.exists() && choseDir.isDirectory()) {
openProject(choseDir);
} else {
getApp().toast(R.string.msg_picked_isnt_dir, Toaster.Type.ERROR);
}
} else {
getApp().toast(R.string.msg_pick_single_file, Toaster.Type.ERROR);
}
}
}
}
private void createNewProject() {
final String appName =
Objects.requireNonNull(createLayoutBinding.createprojectTextAppName.getEditText())
.getText()
.toString()
.trim();
final String packageName =
Objects.requireNonNull(
createLayoutBinding.createprojectTextPackageName.getEditText())
.getText()
.toString()
.trim();
final int minSdk = getMinSdk();
final int targetSdk = getTargetSdk();
if (!isValid(appName, packageName, minSdk, targetSdk)) {
getApp().toast(R.string.invalid_values, Toaster.Type.ERROR);
return;
}
createProject(appName, packageName, minSdk, targetSdk);
}
private void createProject(String appName, String packageName, int minSdk, int targetSdk) {
new TaskExecutor()
.executeAsync(
new ProjectCreatorCallable(
mTemplates.get(currentTemplateIndex),
new NewProjectDetails(appName, packageName, minSdk, targetSdk),
this),
r -> {});
}
private boolean isValid(String appName, String packageName, int minSdk, int targetSdk) {
return appName != null
&& appName.length() > 0
&& packageName != null
&& packageName.length() > 0
&& minSdk > 1
&& minSdk < 99
&& targetSdk > 1
&& targetSdk < 99;
}
private int getMinSdk() {
try {
return Integer.parseInt(
Objects.requireNonNull(
createLayoutBinding.createprojectTextMinSdk.getEditText())
.getText()
.toString());
} catch (Exception e) {
getApp().toast(e.getMessage(), Toaster.Type.ERROR);
}
return -1;
}
private int getTargetSdk() {
try {
return Integer.parseInt(
Objects.requireNonNull(
createLayoutBinding.createprojectTextTargetSdk.getEditText())
.getText()
.toString());
} catch (Exception e) {
getApp().toast(e.getMessage(), Toaster.Type.ERROR);
}
return -1;
}
private void showCreateProject() {
MaterialContainerTransform transform =
TransformUtils.createContainerTransformFor(
binding.createProject, binding.createNewCard, binding.realContainer);
TransitionManager.beginDelayedTransition(binding.getRoot(), transform);
binding.createNewCard.setVisibility(View.VISIBLE);
}
private void hideCreateProject() {
MaterialContainerTransform transform =
TransformUtils.createContainerTransformFor(
binding.createNewCard, binding.createProject, binding.realContainer);
TransitionManager.beginDelayedTransition(binding.getRoot(), transform);
binding.createNewCard.setVisibility(View.GONE);
}
private static final class ProjectReader implements Callable<AndroidProject> {
private final File root;
public ProjectReader(File root) {
this.root = root;
}
@Override
public AndroidProject call() throws Exception {
return ProjectFinder.fromFolder(root);
@Override
protected void onResume() {
super.onResume();
if (mProgressSheet != null && mProgressSheet.isShowing()) {
mProgressSheet.dismiss();
}
}
}
@@ -18,8 +18,6 @@
package com.itsaky.androidide.actions.editor
import android.content.Context
import android.content.res.TypedArray
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseEditorAction
@@ -1,171 +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.adapters;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.ChangeBounds;
import androidx.transition.TransitionManager;
import com.blankj.utilcode.util.FileUtils;
import com.bumptech.glide.Glide;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.LayoutProjectsItemBinding;
import com.itsaky.androidide.projects.AndroidProject;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.toaster.Toaster;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class ProjectsListAdapter extends RecyclerView.Adapter<ProjectsListAdapter.VH> {
private ArrayList<AndroidProject> mProjects;
private OnProjectClickListener listener;
private final Comparator<AndroidProject> PROJECTS_SORTER =
new Comparator<AndroidProject>() {
@Override
public int compare(AndroidProject p1, AndroidProject p2) {
return p1.getAppName().compareTo(p2.getAppName());
}
};
public ProjectsListAdapter(
ArrayList<AndroidProject> mProjects, OnProjectClickListener listener) {
this.mProjects = mProjects;
this.listener = listener;
Collections.sort(this.mProjects, PROJECTS_SORTER);
}
@Override
public ProjectsListAdapter.VH onCreateViewHolder(ViewGroup p1, int p2) {
return new VH(
LayoutProjectsItemBinding.inflate(LayoutInflater.from(p1.getContext()), p1, false));
}
@Override
public void onBindViewHolder(ProjectsListAdapter.VH holder, int p2) {
final AndroidProject project = mProjects.get(p2);
holder.binding.projecsitemName.setText(project.getAppName());
holder.binding.projecsitemPkgName.setText(project.getPackageName());
holder.binding.deleteMsg.setText(
holder.binding
.deleteMsg
.getContext()
.getString(R.string.msg_confirm_delete, project.getAppName()));
if (!TextUtils.isEmpty(project.getIconPath()))
Glide.with(holder.itemView.getContext().getApplicationContext())
.load(new File(project.getIconPath()))
.into(holder.binding.projecsitemIcon);
else holder.binding.projecsitemIcon.setImageResource(R.drawable.ic_android);
holder.binding.projectsitemClickableContainer.setOnClickListener(
v -> sendClickEvent(project));
holder.binding.projectsitemClickableContainer.setOnLongClickListener(
v -> holder.toggleDelete());
holder.binding.delete.setOnClickListener(v -> holder.showConfirmDelete());
holder.binding.deleteCancel.setOnClickListener(v -> holder.hideAllDelete());
holder.binding.deleteOk.setOnClickListener(v -> deleteProject(project, p2));
}
private void deleteProject(AndroidProject project, int index) {
index = index >= mProjects.size() ? mProjects.size() - 1 : index;
if (index < 0) return;
mProjects.remove(index);
notifyItemRemoved(index);
new TaskExecutor()
.executeAsync(
() -> FileUtils.delete(project.getProjectPath()),
__ -> {
StudioApp.getInstance()
.toast(
__
? R.string.msg_project_deleted
: R.string.msg_project_delete_failed,
Toaster.Type.SUCCESS);
});
}
private void sendClickEvent(AndroidProject project) {
if (listener != null) listener.onProjectClick(project);
}
@Override
public int getItemCount() {
return mProjects.size();
}
public class VH extends RecyclerView.ViewHolder {
LayoutProjectsItemBinding binding;
public VH(LayoutProjectsItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void showConfirmDelete() {
binding.confirmDeleteContainer.setEnabled(true);
TransitionManager.beginDelayedTransition(
binding.getRoot(), new ChangeBounds().setDuration(85));
binding.confirmDeleteContainer.setVisibility(View.VISIBLE);
}
public void hideAllDelete() {
hideDelete();
hideConfirmDelete();
}
public boolean toggleDelete() {
binding.delete.setEnabled(!binding.delete.isEnabled());
TransitionManager.beginDelayedTransition(
binding.getRoot(), new ChangeBounds().setDuration(85));
binding.delete.setVisibility(binding.delete.isEnabled() ? View.VISIBLE : View.GONE);
return true;
}
public void hideDelete() {
binding.delete.setEnabled(false);
TransitionManager.beginDelayedTransition(
binding.getRoot(), new ChangeBounds().setDuration(85));
binding.delete.setVisibility(View.GONE);
}
public void hideConfirmDelete() {
binding.confirmDeleteContainer.setEnabled(false);
TransitionManager.beginDelayedTransition(
binding.getRoot(), new ChangeBounds().setDuration(85));
binding.confirmDeleteContainer.setVisibility(View.GONE);
}
}
public interface OnProjectClickListener {
public void onProjectClick(AndroidProject project);
}
}
@@ -40,7 +40,7 @@ import com.itsaky.androidide.R;
import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.LayoutEditorFileTreeBinding;
import com.itsaky.androidide.projects.AndroidProject;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.callables.FileTreeCallable;
import com.itsaky.androidide.utils.Environment;
@@ -63,19 +63,27 @@ public class FileTreeFragment extends BottomSheetDialogFragment
private LayoutEditorFileTreeBinding binding;
private AndroidTreeView mFileTreeView;
private FileActionListener mFileActionListener;
private AndroidProject mProject;
private TreeNode mRoot;
private String mTreeState;
public FileTreeFragment() {}
@NonNull
public static FileTreeFragment newInstance(AndroidProject project) {
Bundle bundle = new Bundle();
bundle.putParcelable("project", project);
FileTreeFragment frag = new FileTreeFragment();
frag.setArguments(bundle);
return frag;
public static FileTreeFragment newInstance() {
return new FileTreeFragment();
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mFileActionListener = (FileActionListener) context;
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
saveTreeState();
outState.putString(KEY_STORED_TREE_STATE, mTreeState);
}
public void saveTreeState() {
@@ -87,49 +95,6 @@ public class FileTreeFragment extends BottomSheetDialogFragment
}
}
private void tryRestoreState() {
tryRestoreState(mTreeState);
}
private void tryRestoreState(String state) {
if (!TextUtils.isEmpty(state) && mFileTreeView != null) {
LOG.debug("Restoring tree view state:", "'" + state + "'");
mFileTreeView.collapseAll();
final var openNodesArray = state.split(NODES_PATH_SEPARATOR);
final var openNodes = new HashSet<>(Arrays.asList(openNodesArray));
restoreNodeState(mRoot, openNodes);
} else {
LOG.error(
"Unable to restore tree state",
"treeState=" + state,
"treeView=" + mFileTreeView);
}
}
private void restoreNodeState(@NonNull TreeNode root, Set<String> openNodes) {
final var children = root.getChildren();
for (int i = 0, childrenSize = children.size(); i < childrenSize; i++) {
final var node = children.get(i);
if (openNodes.contains(node.getPath())) {
listNode(
node,
() -> {
updateChevron(node);
expandNode(node);
restoreNodeState(node, openNodes);
});
}
}
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mFileActionListener = (FileActionListener) context;
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -140,7 +105,6 @@ public class FileTreeFragment extends BottomSheetDialogFragment
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mProject = requireArguments().getParcelable("project");
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_STORED_TREE_STATE)) {
mTreeState = savedInstanceState.getString(KEY_STORED_TREE_STATE, null);
@@ -188,11 +152,28 @@ public class FileTreeFragment extends BottomSheetDialogFragment
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
saveTreeState();
outState.putString(KEY_STORED_TREE_STATE, mTreeState);
public void collapseNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.collapseNode(node);
}
public void expandNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.expandNode(node);
}
private void setLoading(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).setLoading();
}
}
private void listNode(@NonNull TreeNode node, Runnable whenDone) {
@@ -222,6 +203,12 @@ public class FileTreeFragment extends BottomSheetDialogFragment
__ -> finalWhenDone.run());
}
private void updateChevron(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).updateChevron(!node.isExpanded());
}
}
private void getNodeFromFiles(File[] files, TreeNode parent) {
Arrays.sort(files, new FileTreeCallable.SortFileName());
Arrays.sort(files, new FileTreeCallable.SortFolder());
@@ -232,18 +219,6 @@ public class FileTreeFragment extends BottomSheetDialogFragment
}
}
private void setLoading(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).setLoading();
}
}
private void updateChevron(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).updateChevron(!node.isExpanded());
}
}
@Override
public boolean onLongClick(TreeNode node, Object value) {
if (mFileActionListener != null) {
@@ -252,32 +227,15 @@ public class FileTreeFragment extends BottomSheetDialogFragment
return true;
}
public void expandNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.expandNode(node);
}
public void collapseNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.collapseNode(node);
}
public void listProjectFiles() {
if (mProject == null) {
final var projectDirPath = ProjectManager.INSTANCE.getProjectDir();
if (projectDirPath == null) {
return;
}
final File gradleProps = Environment.GRADLE_PROPS;
final File gradleHome = Environment.GRADLE_USER_HOME;
File projectDir = new File(mProject.getProjectPath());
File projectDir = new File(projectDirPath);
mRoot = TreeNode.root(projectDir);
if (gradleHome.exists() && gradleHome.isDirectory()) {
if (!gradleProps.exists()) {
@@ -342,6 +300,43 @@ public class FileTreeFragment extends BottomSheetDialogFragment
return binding.fileTreeLoadingProgress;
}
private void tryRestoreState() {
tryRestoreState(mTreeState);
}
private void tryRestoreState(String state) {
if (!TextUtils.isEmpty(state) && mFileTreeView != null) {
LOG.debug("Restoring tree view state:", "'" + state + "'");
mFileTreeView.collapseAll();
final var openNodesArray = state.split(NODES_PATH_SEPARATOR);
final var openNodes = new HashSet<>(Arrays.asList(openNodesArray));
restoreNodeState(mRoot, openNodes);
} else {
LOG.error(
"Unable to restore tree state",
"treeState=" + state,
"treeView=" + mFileTreeView);
}
}
private void restoreNodeState(@NonNull TreeNode root, Set<String> openNodes) {
final var children = root.getChildren();
for (int i = 0, childrenSize = children.size(); i < childrenSize; i++) {
final var node = children.get(i);
if (openNodes.contains(node.getPath())) {
listNode(
node,
() -> {
updateChevron(node);
expandNode(node);
restoreNodeState(node, openNodes);
});
}
}
}
public interface FileActionListener {
CodeEditorView openFile(File file);
@@ -17,7 +17,6 @@
*/
package com.itsaky.androidide.fragments.preferences;
import com.itsaky.androidide.managers.PreferenceManager;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_AUTO_SAVE;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_DRAW_HEX;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_LINE_BREAK;
@@ -44,6 +43,7 @@ import androidx.preference.SwitchPreference;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.R;
import com.itsaky.androidide.databinding.LayoutTextSizeSliderBinding;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.utils.DialogUtils;
@@ -29,6 +29,7 @@ import com.itsaky.androidide.databinding.LayoutCreateFileJavaBinding;
import com.itsaky.androidide.databinding.LayoutDialogTextInputBinding;
import com.itsaky.androidide.fragments.sheets.OptionsListFragment;
import com.itsaky.androidide.models.SheetOption;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.ProjectWriter;
@@ -93,34 +94,27 @@ public class FileOptionsHandler extends IDEHandler
if (forceUnknownType) {
createNewEmptyFile(f);
} else {
final var projectDir = ProjectManager.INSTANCE.getProjectDir();
Objects.requireNonNull(projectDir);
final boolean isJava =
Pattern.compile(
Pattern.quote(androidProject().getProjectPath())
+ JAVA_PATH_REGEX)
Pattern.compile(Pattern.quote(projectDir) + JAVA_PATH_REGEX)
.matcher(f.getAbsolutePath())
.find();
final boolean isRes =
Pattern.compile(
Pattern.quote(androidProject().getProjectPath())
+ RES_PATH_REGEX)
Pattern.compile(Pattern.quote(projectDir) + RES_PATH_REGEX)
.matcher(f.getAbsolutePath())
.find();
final boolean isLayoutRes =
Pattern.compile(
Pattern.quote(androidProject().getProjectPath())
+ LAYOUT_RES_PATH_REGEX)
Pattern.compile(Pattern.quote(projectDir) + LAYOUT_RES_PATH_REGEX)
.matcher(f.getAbsolutePath())
.find();
final boolean isMenuRes =
Pattern.compile(
Pattern.quote(androidProject().getProjectPath())
+ MENU_RES_PATH_REGEX)
Pattern.compile(Pattern.quote(projectDir) + MENU_RES_PATH_REGEX)
.matcher(f.getAbsolutePath())
.find();
final boolean isDrawableRes =
Pattern.compile(
Pattern.quote(androidProject().getProjectPath())
+ DRAWABLE_RES_PATH_REGEX)
Pattern.compile(Pattern.quote(projectDir) + DRAWABLE_RES_PATH_REGEX)
.matcher(f.getAbsolutePath())
.find();
if (isJava) {
@@ -18,8 +18,6 @@
package com.itsaky.androidide.handlers;
import com.itsaky.androidide.EditorActivity;
import com.itsaky.androidide.projects.AndroidProject;
import com.itsaky.androidide.projects.IDEProject;
import com.itsaky.androidide.utils.ILogger;
/**
@@ -36,22 +34,14 @@ public abstract class IDEHandler {
this.provider = provider;
}
protected EditorActivity activity() {
return provider.provideEditorActivity();
}
protected AndroidProject androidProject() {
return provider.provideAndroidProject();
}
protected IDEProject ideProject() {
return provider.provideIDEProject();
}
public abstract void start();
public abstract void stop();
protected EditorActivity activity() {
return provider.provideEditorActivity();
}
/** An interface to communicate between a handler and its client */
public interface Provider {
@@ -61,19 +51,5 @@ public abstract class IDEHandler {
* @throws NullPointerException is this is required
*/
EditorActivity provideEditorActivity();
/**
* Called by handler to get a reference of the current {@link AndroidProject}
*
* @throws NullPointerException is this is required
*/
AndroidProject provideAndroidProject();
/**
* Called by handler to get a reference of the current {@link IDEProject}
*
* @throws NullPointerException is this is required
*/
IDEProject provideIDEProject();
}
}
@@ -18,7 +18,6 @@
package com.itsaky.androidide.language.xml;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.OPERATOR;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.TEXT_SELECTED;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.forComment;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.get;
import static io.github.rosemoe.sora.widget.schemes.EditorColorScheme.LITERAL;
@@ -25,8 +25,8 @@ import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.language.CommonCompletionProvider;
import com.itsaky.androidide.language.IDELanguage;
import com.itsaky.androidide.lexers.xml.XMLLexer;
import com.itsaky.androidide.utils.JavaCharacter;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.JavaCharacter;
import com.itsaky.androidide.views.editor.IDEEditor;
import com.itsaky.lsp.api.ILanguageServer;
@@ -38,8 +38,8 @@ import com.itsaky.androidide.models.DiagnosticGroup;
import com.itsaky.androidide.models.SearchResult;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.views.editor.CodeEditorView;
import com.itsaky.androidide.views.editor.IDEEditor;
import com.itsaky.lsp.api.ILanguageClient;
@@ -1,225 +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.projects;
import android.os.Parcel;
import android.os.Parcelable;
import com.itsaky.androidide.app.StudioApp;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class AndroidProject implements Parcelable {
private String appName;
private String packageName;
private String iconPath;
private String projectPath;
private String mainModule;
private String mainSourcePath;
private List<String> modulePaths = new ArrayList<>();
private List<String> sourcePaths = new ArrayList<>();
private List<String> classPaths = new ArrayList<>();
public AndroidProject() {
addSource(StudioApp.getInstance().getLogSenderDir().getAbsolutePath());
}
protected AndroidProject(Parcel in) {
appName = in.readString();
packageName = in.readString();
iconPath = in.readString();
projectPath = in.readString();
mainModule = in.readString();
mainSourcePath = in.readString();
in.readList(modulePaths, String.class.getClassLoader());
in.readList(sourcePaths, String.class.getClassLoader());
in.readList(classPaths, String.class.getClassLoader());
}
public File getBuildDir() {
return new File(getMainModulePath(), "build");
}
public AndroidProject setMainModule(String mainModule) {
this.mainModule = mainModule;
return this;
}
public String getMainModule() {
return mainModule;
}
public String getMainModulePath() {
return new File(getProjectPath(), getMainModule()).getAbsolutePath();
}
public AndroidProject setMainSourcePath(String mainModulePath) {
this.mainSourcePath = mainModulePath;
return this;
}
public String getMainSourcePath() {
return mainSourcePath;
}
public AndroidProject setModulePaths(List<String> modulePaths) {
this.modulePaths = modulePaths;
return this;
}
public List<String> getModulePaths() {
if (modulePaths == null) modulePaths = new ArrayList<>();
return modulePaths;
}
public AndroidProject addModule(String path) {
if (!getModulePaths().contains(path)) getModulePaths().add(path);
return this;
}
public AndroidProject setSourcePaths(List<String> sourcePaths) {
this.sourcePaths = sourcePaths;
return this;
}
public List<String> getSourcePaths() {
if (sourcePaths == null) sourcePaths = new ArrayList<>();
return sourcePaths;
}
public AndroidProject addSource(String path) {
if (!getSourcePaths().contains(path)) getSourcePaths().add(path);
return this;
}
public AndroidProject setClassPaths(List<String> classPaths) {
this.classPaths = classPaths;
return this;
}
public List<String> getClassPaths() {
if (classPaths == null) classPaths = new ArrayList<>();
return classPaths;
}
public AndroidProject addClasspath(String path) {
if (!getClassPaths().contains(path)) getClassPaths().add(path);
return this;
}
public AndroidProject setAppName(String appName) {
this.appName = appName;
return this;
}
public String getAppName() {
return appName;
}
public AndroidProject setPackageName(String packageName) {
this.packageName = packageName;
return this;
}
public String getPackageName() {
return packageName;
}
public AndroidProject setIconPath(String iconPath) {
this.iconPath = iconPath;
return this;
}
public String getIconPath() {
return iconPath;
}
public AndroidProject setProjectPath(String projectPath) {
this.projectPath = projectPath;
return this;
}
public String getProjectPath() {
return projectPath;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel p1, int p2) {
p1.writeString(appName);
p1.writeString(packageName);
p1.writeString(iconPath);
p1.writeString(projectPath);
p1.writeString(mainModule);
p1.writeString(mainSourcePath);
p1.writeList(modulePaths);
p1.writeList(sourcePaths);
p1.writeList(classPaths);
}
@Override
public String toString() {
return "["
+ "\nappName="
+ getAppName()
+ "\npackageName="
+ getPackageName()
+ "\niconPath="
+ getIconPath()
+ "\nprojectPath="
+ getProjectPath()
+ "\nmainModule="
+ getMainModule()
+ "\nmainModulePath="
+ getMainModulePath()
+ "\nmainModuleSourcePath="
+ getMainSourcePath()
+ "\nmodules="
+ getModulePaths()
+ "\nsources="
+ getSourcePaths()
+ "\nclasspaths="
+ getClassPaths()
+ "\n]";
}
public static final Creator<AndroidProject> CREATOR =
new Creator<AndroidProject>() {
@Override
public AndroidProject createFromParcel(Parcel p1) {
return new AndroidProject(p1);
}
@Override
public AndroidProject[] newArray(int p1) {
return new AndroidProject[p1];
}
};
}
@@ -1,68 +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.projects;
import com.google.gson.annotations.SerializedName;
/**
* Represents a module (subproject) of a root project.
*
* <p>In Android projects, a module can either be an application project, or a library project.
*/
public class IDEModule extends IDEProject {
/** Build tools version of this project */
@SerializedName("buildToolsVersion")
public String buildToolsVersion;
/**
* Compile SDK version of this project
*
* <p>Representation: 'android-31', 'android-30', 'android-29', etc
*/
@SerializedName("compileSdkVersion")
public String compileSdkVersion;
/** Minimum SDK version of this project */
@SerializedName("minSdk")
public SDK minSdk;
/** Target SDK version of this project */
@SerializedName("targetSdk")
public SDK targetSdk;
/** Version code of this project */
@SerializedName("versionCode")
public int versionCode = 0;
/** Version name of this project */
@SerializedName("versionName")
public String versionName = "Not defined";
/** Is this project an Android library project? */
@SerializedName("isLibrary")
public boolean isLibrary = true;
/** Does this module has viewBinding enabled? */
@SerializedName("viewBindingEnabled")
public boolean viewBindingEnabled;
/** Library projects don't have applicationId, application projects do. */
@SerializedName("applicationId")
public String applicationId;
}
@@ -1,96 +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.projects;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/** A model that contains data of a project currently opened in AndroidIDE */
public class IDEProject {
/** Name of this project */
@SerializedName("name")
public String name = "";
/** Name of this project which should be displayed to user */
@SerializedName("displayName")
public String displayName = "";
/** Description of this project. Maybe null. */
@SerializedName("description")
public String description = "";
/**
* Path of this project. This is NOT the directory path of this project.
*
* <p>For example, ':app or ':module:subModule'
*/
@SerializedName("path")
public String path = "";
/** Path of the directory containing this project */
@SerializedName("projectDir")
public String projectDir;
/** File path of the application's icon We get this manually by parsing AndroidManifest.xml */
public String iconPath;
/** Subprojects of this project */
@SerializedName("modules")
public List<IDEModule> modules = new ArrayList<>();
/** Tasks included in this project */
@SerializedName("tasks")
public List<IDETask> tasks = new ArrayList<>();
/** Path of jar file of dependencies of this project */
@SerializedName("dependencies")
public List<String> dependencies = new ArrayList<>();
/**
* Get the module by its path
*
* @param path Path of the module
* @return An Optional containing the IDEModule or Optional.empty()
*/
public Optional<IDEModule> getModuleByPath(String path) {
if (modules != null && modules.size() > 0) {
for (IDEModule module : modules) {
if (module == null) continue;
if (module.path.trim().equals(path.trim())) return Optional.of(module);
}
}
return Optional.empty();
}
/** Stores information about a SDK. Like, • Codename • API string • API level */
public static class SDK {
@SerializedName("codename")
public String codename;
@SerializedName("apiString")
public String apiString;
@SerializedName("apiLevel")
public int apiLevel;
}
}
@@ -1,44 +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.projects;
import com.google.gson.annotations.SerializedName;
/** Represents a Gradle task */
public class IDETask {
/** Name of this task */
@SerializedName("name")
public String name;
/** Description of this task */
@SerializedName("description")
public String description;
/** Name of the group that this task belongs to */
@SerializedName("group")
public String group;
/**
* Path of this task
*
* <p>For example, ':app:assembleDebug', ':app:build'
*/
@SerializedName("path")
public String path;
}
@@ -1,38 +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.projects;
import android.os.FileObserver;
/**
* A file observer that observer files in project directories. Sub classes need to provide the file
* extensions that they want to get notified about.
*
* @author Akash Yadav
*/
public abstract class ProjectFileObserver extends FileObserver {
// Cannot use constructors that accept java.io.File
// They were added in API 29
public ProjectFileObserver(String path) {
this(path, ALL_EVENTS);
}
public ProjectFileObserver(String path, int mask) {
super(path, mask);
}
}
@@ -36,16 +36,64 @@ import java.nio.file.Path
object ProjectManager {
private val log = com.itsaky.androidide.utils.ILogger.newInstance(javaClass.simpleName)
var rootProject: IdeGradleProject? = null
var projectPath: String? = null
fun getProjectDir(): String? =
if (rootProject == null) projectPath else rootProject!!.projectDir!!.absolutePath
fun getApplicationModule(): IdeAndroidModule? {
if (rootProject == null) {
log.error(
"No root project instance is set. Is the project initialization process finished?")
return null
}
val app = rootProject!!.findFirstAndroidModule()
if (app == null) {
log.error("No application module found in root project.")
return null
}
return app
}
fun getApplicationResDirectories(): List<File>? {
return collectResDirectories(getApplicationModule() ?: return null)
}
fun collectResDirectories(android: IdeAndroidModule): List<File> {
val main = android.mainSourceSet
if (main == null) {
log.error("No main source set found in application module: ${android.name}")
return emptyList()
}
val dirs = mutableListOf<File>()
if (main.sourceProvider.resDirectories != null) {
dirs.addAll(main.sourceProvider.resDirectories!!)
}
val dependencies =
collectProjectDependencies(rootProject!!, android)
.filterIsInstance(IdeAndroidModule::class.java)
for (dependency in dependencies) {
dirs.addAll(collectResDirectories(dependency))
}
return dirs
}
fun notifyProjectUpdate(server: JavaLanguageServer) {
val sourceDirs = collectApplicationSourceDirs(rootProject!!)
val classPaths = collectApplicationClassPaths(rootProject!!)
val sourceDirs = collectApplicationSourceDirs()
val classPaths = collectApplicationClassPaths()
val configuration = JavaServerConfiguration(classPaths, sourceDirs)
server.configurationChanged(configuration)
}
private fun collectApplicationClassPaths(rootProject: IdeGradleProject): Set<Path> {
val app = getApplicationModule(rootProject)
private fun collectApplicationClassPaths(): Set<Path> {
val app = getApplicationModule() ?: return emptySet()
val libraries = app.variantDependencies["debug"]!!.libraries
val paths = mutableSetOf<Path>()
@@ -55,7 +103,8 @@ object ProjectManager {
} else if (value.type == JAVA_LIBRARY) {
paths.add(value.artifact!!.toPath())
} else {
val project = rootProject.findByPath(value.projectInfo!!.projectPath)!! as IdeModule
val project =
rootProject!!.findByPath(value.projectInfo!!.projectPath)!! as IdeModule
paths.add(project.getGeneratedJar("debug").toPath())
if (project is IdeAndroidModule && project.projectPath != app.projectPath) {
@@ -73,22 +122,16 @@ object ProjectManager {
return paths
}
private fun collectApplicationSourceDirs(rootProject: IdeGradleProject): Set<Path> {
val app = getApplicationModule(rootProject)
private fun collectApplicationSourceDirs(): Set<Path> {
val app = getApplicationModule() ?: return emptySet()
val sources = app.mainSourceSet ?: return emptySet()
val javaDirs = sources.sourceProvider.javaDirectories
val sourcePaths = javaDirs.toMutableSet()
val projectSources = collectSourceDirs(collectProjectDependencies(rootProject, app))
val projectSources = collectSourceDirs(collectProjectDependencies(rootProject!!, app))
sourcePaths.addAll(projectSources)
return sourcePaths.map { it.toPath() }.toSet()
}
private fun getApplicationModule(rootProject: IdeGradleProject): IdeAndroidModule {
return rootProject.findFirstAndroidModule()
?: throw UnsupportedOperationException(
"No application module found in project ${rootProject.name} (${rootProject.projectPath})")
}
fun collectProjectDependencies(
project: IdeGradleProject,
app: IdeAndroidModule
@@ -1,83 +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.services.builder;
import com.itsaky.androidide.projects.IDEModule;
import com.itsaky.androidide.projects.IDEProject;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Optional;
/** A client should implement this interface to get callbacks about a build */
public interface BuildListener {
/** Called to notify the client about the modification of build/generated files */
void onBuildModified();
/**
* Called when the project has been loaded and its data has been read
*
* @param project The loaded project
* @param appModule The main application module
*/
void onProjectLoaded(IDEProject project, Optional<IDEModule> appModule);
/**
* Called when a Gradle Daemon is being started
*
* @param task Task that is currently running
*/
void onStartingGradleDaemon(GradleTask task);
/**
* Called when a task is started
*
* @param task Task that is currently running
* @param name Name of this task that should be shown to user
*/
void onRunTask(GradleTask task, String name);
/**
* Notifys about a successful build
*
* @param task Task that succeeded
* @param msg Successful message
*/
void onBuildSuccessful(GradleTask task, String msg);
/**
* Notifys about a failed build
*
* @param task Task that failed
* @param msg Failure message
*/
void onBuildFailed(GradleTask task, String msg);
/** Called to notify client to save any opened files */
void saveFiles();
/**
* Notifys the client to append the build output that should be shown to user
*
* @param task Task that is currently running
* @param text Output to append
*/
void appendOutput(GradleTask task, CharSequence text);
/** Called to notify the client to prepare for a build */
void prepareBuild();
}
@@ -1,493 +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.services.builder;
import static com.itsaky.androidide.managers.ToolsManager.getCommonAsset;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.ResourceUtils;
import com.blankj.utilcode.util.ZipUtils;
import com.google.gson.Gson;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.projects.IDEModule;
import com.itsaky.androidide.projects.IDEProject;
import com.itsaky.androidide.shell.IProcessExecutor;
import com.itsaky.androidide.shell.IProcessExitListener;
import com.itsaky.androidide.shell.ProcessExecutorFactory;
import com.itsaky.androidide.shell.ProcessStreamsHolder;
import com.itsaky.androidide.tasks.GradleTask;
import com.itsaky.androidide.tasks.gradle.BaseGradleTasks;
import com.itsaky.androidide.tasks.gradle.resources.UpdateResourceClassesTask;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.InputStreamLineReader;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.toaster.Toaster;
import org.jetbrains.annotations.Contract;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class IDEService {
public static final int TASK_SHOW_DEPENDENCIES = GradleTask.TASK_SHOW_DEPENDENCIES;
public static final int TASK_ASSEMBLE_DEBUG = GradleTask.TASK_ASSEMBLE_DEBUG;
public static final int TASK_ASSEMBLE_RELEASE = GradleTask.TASK_ASSEMBLE_RELEASE;
public static final int TASK_BUILD = GradleTask.TASK_BUILD;
public static final int TASK_BUNDLE = GradleTask.TASK_BUNDLE;
public static final int TASK_CLEAN = GradleTask.TASK_CLEAN;
public static final int TASK_CLEAN_BUILD = GradleTask.TASK_CLEAN_BUILD;
public static final int TASK_DEX = GradleTask.TASK_DEX;
public static final int TASK_LINT = GradleTask.TASK_LINT;
public static final int TASK_LINT_DEBUG = GradleTask.TASK_LINT_DEBUG;
public static final int TASK_LINT_RELEASE = GradleTask.TASK_LINT_RELEASE;
public static final ILogger LOG = ILogger.newInstance("IDEService");
private final File projectRoot;
private final IProcessExecutor processExecutor;
private BuildListener listener;
private GradleTask currentTask;
private IDEProject mIDEProject;
private IDEModule mAppModule;
private final InputStreamLineReader.OnReadListener mOutputReadListener = this::onBuildOutput;
private boolean isBuilding = false;
private final IProcessExitListener mProcessExitListener =
new IProcessExitListener() {
@Override
public void onExit(int code) {
isBuilding = false;
if (listener != null) {
final String msg = getString(R.string.msg_gradle_terminated, code);
if (code == 0) {
onBuildSuccessful(currentTask, msg);
} else {
onBuildFailed(currentTask, msg);
}
}
}
};
public IDEService(File projectRoot) {
this.projectRoot = projectRoot;
this.processExecutor = ProcessExecutorFactory.commonExecutor();
}
public IDEService setListener(final BuildListener listener) {
this.listener = listener == null ? null : new MainThreadBuildListener(listener);
return this;
}
protected void onBuildOutput(String line) {
if (listener == null || line == null) return;
line = line.trim();
String PROJECT_INITIALIZED = ">>> PROJECT INITIALIZED <<<";
if (line.contains(PROJECT_INITIALIZED)) {
readIdeProject();
return;
}
String text = line;
final var app = StudioApp.getInstance();
// Try to shorten common file paths
if (line.contains(app.getRootDir().getAbsolutePath())) {
text = line.replace(app.getRootDir().getAbsolutePath(), "HOME");
}
if (line.contains(
Objects.requireNonNull(app.getRootDir().getParentFile()).getAbsolutePath())) {
text = line.replace(app.getRootDir().getParentFile().getAbsolutePath(), "ROOT");
}
listener.appendOutput(currentTask, text);
String RUN_TASK = "> Task";
String STARTING_DAEMON = "Starting a ";
if (line.startsWith(RUN_TASK)) {
listener.onRunTask(currentTask, line.trim());
} else if (line.startsWith(STARTING_DAEMON)) {
listener.onStartingGradleDaemon(currentTask);
}
}
protected void onBuildSuccessful(GradleTask task, String msg) {
if (currentTask != null && currentTask.getTaskID() != TASK_SHOW_DEPENDENCIES) {
listener.onBuildSuccessful(task, msg.trim());
}
appendOutputSeparator();
if (currentTask != null && currentTask.affectsGeneratedSources()) {
notifyExternalSourceChange();
}
}
protected void onBuildFailed(GradleTask task, @NonNull String msg) {
isBuilding = false;
listener.onBuildFailed(task, msg.trim());
appendOutputSeparator();
}
private void readIdeProject() {
// Parsing project data is meaningless if there is no one listening...
if (listener == null) {
return;
}
final BuildListener local = listener;
new Thread(
() -> {
mIDEProject =
new Gson()
.fromJson(
FileIOUtils.readFile2String(
Environment.PROJECT_DATA_FILE),
IDEProject.class);
if (mIDEProject != null) {
Optional<IDEModule> appModule = mIDEProject.getModuleByPath(":app");
appModule.ifPresent(ideModule -> mAppModule = ideModule);
local.onProjectLoaded(mIDEProject, appModule);
} else {
StudioApp.getInstance()
.toast(
com.itsaky.androidide.R.string
.msg_init_project_failed,
Toaster.Type.ERROR);
}
})
.start();
}
private void appendOutputSeparator() {
listener.appendOutput(currentTask, "\n\n");
}
public void exit() {
// Delete the tmp directory. It is not needed anymore...
FileUtils.delete(Environment.TMP_DIR);
}
@NonNull
private String currentTime() {
String pattern = "HH:mm:ss";
DateFormat df = new SimpleDateFormat(pattern, Locale.US);
Date today = Calendar.getInstance().getTime();
return df.format(today);
}
private String getString(@StringRes int id) {
return StudioApp.getInstance().getString(id);
}
private String getString(@StringRes int id, Object... format) {
return StudioApp.getInstance().getString(id, format);
}
private void notifyExternalSourceChange() {
// TODO Watch for file changes using a file observer
// instead of depending on the build
if (listener != null) {
listener.onBuildModified();
}
}
@NonNull
private String[] getArguments(List<String> tasks) {
final PreferenceManager prefs = StudioApp.getInstance().getPrefManager();
final List<String> args = new ArrayList<>();
// 'bash' shell is always preferred
if (Environment.SHELL.exists()) {
args.add(Environment.SHELL.getAbsolutePath());
} else {
args.add(Environment.BUSYBOX.getAbsolutePath());
args.add("sh");
}
args.add(new File(projectRoot, "gradlew").getAbsolutePath());
args.addAll(asAppTasks(tasks));
args.add("--init-script");
args.add(Environment.INIT_SCRIPT.getAbsolutePath());
if (prefs.isStackTraceEnabled()) {
args.add("--stacktrace");
}
if (prefs.isGradleInfoEnabled()) {
args.add("--info");
}
if (prefs.isGradleDebugEnabled()) {
args.add("--debug");
}
if (prefs.isGradleScanEnabled()) {
args.add("--scan");
}
if (prefs.isGradleWarningEnabled()) {
args.add("--warning-mode");
args.add("all");
}
if (prefs.isGradleBuildCacheEnabled()) {
args.add("--build-cache");
}
if (prefs.isGradleOfflineModeEnabled()) {
args.add("--offline");
}
return args.toArray(new String[0]);
}
private Collection<? extends String> asAppTasks(@NonNull List<String> tasks) {
return tasks.stream()
.filter(Objects::nonNull)
.map(this::asAppModuleTask)
.collect(Collectors.toList());
}
@Contract(pure = true)
private String asAppModuleTask(@NonNull String name) {
return name.startsWith(":app:") ? name : ":app:" + name;
}
public void execTask(GradleTask task) {
execTask(task, false);
}
public void execTask(GradleTask task, boolean stopIfRunning) {
if (stopIfRunning && isBuilding()) {
stopAllDaemons();
} else if (isBuilding()) {
return;
}
execTask(
task,
isGradleWrapperAvailable()
? CompletableFuture.completedFuture(Boolean.TRUE)
: installWrapper());
}
private void execTask(GradleTask task, CompletableFuture<Boolean> installing) {
final Runnable taskRunner =
() -> {
if (installing != null) {
Boolean result;
String failure = "";
try {
result = installing.get();
} catch (Throwable th) {
result = Boolean.FALSE;
failure = th.getMessage();
}
if (result.equals(Boolean.FALSE)) {
if (listener != null) {
listener.appendOutput(
task, "-------------------- ERROR --------------------");
listener.appendOutput(
task,
getString(
R.string.msg_gradlew_installation_failed, failure));
listener.appendOutput(
task, "-----------------------------------------------");
}
return;
}
}
if (listener != null) {
final ProcessStreamsHolder holder = new ProcessStreamsHolder();
listener.saveFiles();
currentTask = task;
listener.appendOutput(
task,
getString(R.string.msg_task_begin, currentTime(), task.getName()));
try {
Environment.mkdirIfNotExits(Environment.TMP_DIR);
processExecutor.execAsync(
holder,
mProcessExitListener,
projectRoot.getAbsolutePath(),
true,
getArguments(task.getTasks()));
isBuilding = true;
startReading(holder.in);
listener.prepareBuild();
} catch (Throwable e) {
listener.onBuildFailed(task, e.getMessage());
}
}
};
final Thread thread = new Thread(taskRunner, "GradleTaskExecutor");
thread.setDaemon(true);
thread.start();
}
private void startReading(InputStream in) {
final Thread reader =
new Thread(
new InputStreamLineReader(in, mOutputReadListener),
"GradleProcessOutputReader");
reader.setDaemon(true);
reader.start();
}
private CompletableFuture<Boolean> installWrapper() {
if (listener != null) {
listener.appendOutput(null, "-------------------- NOTE --------------------");
listener.appendOutput(null, getString(R.string.msg_installing_gradlew));
listener.appendOutput(null, "----------------------------------------------");
}
return CompletableFuture.supplyAsync(
() -> {
final File extracted = new File(Environment.TMP_DIR, "gradle-wrapper.zip");
if (ResourceUtils.copyFileFromAssets(
getCommonAsset("gradle-wrapper.zip"), extracted.getAbsolutePath())) {
try {
final List<File> files = ZipUtils.unzipFile(extracted, projectRoot);
if (files != null && !files.isEmpty()) {
return Boolean.TRUE;
}
} catch (IOException e) {
return Boolean.FALSE;
}
}
return Boolean.FALSE;
});
}
public boolean isGradleWrapperAvailable() {
if (projectRoot == null || !projectRoot.exists()) {
return false;
}
final File gradlew = new File(projectRoot, "gradlew");
final File gradleWrapperJar = new File(projectRoot, "gradle/wrapper/gradle-wrapper.jar");
final File gradleWrapperProps =
new File(projectRoot, "gradle/wrapper/gradle-wrapper.properties");
return gradlew.exists() && gradleWrapperJar.exists() && gradleWrapperProps.exists();
}
public void assembleDebug() {
execTask(BaseGradleTasks.ASSEMBLE_DEBUG);
}
public void assembleRelease() {
execTask(BaseGradleTasks.ASSEMBLE_RELEASE);
}
public void build() {
execTask(BaseGradleTasks.BUILD);
}
public void bundle() {
execTask(BaseGradleTasks.BUNDLE);
}
public void clean() {
execTask(BaseGradleTasks.CLEAN);
}
public void cleanAndRebuild() {
execTask(BaseGradleTasks.CLEAN_BUILD);
}
public void stopAllDaemons() {
StudioApp.getInstance()
.newShell(null)
.bgAppend(
String.format(
Locale.US,
"cd '%s' && sh gradlew --stop",
projectRoot.getAbsolutePath()));
if (isBuilding()) {
if (listener != null)
listener.onBuildFailed(currentTask, getString(R.string.msg_daemons_stopped));
isBuilding = false;
}
}
public void lint() {
execTask(BaseGradleTasks.LINT);
}
public void lintDebug() {
execTask(BaseGradleTasks.LINT_DEBUG);
}
public void lintRelease() {
execTask(BaseGradleTasks.LINT_RELEASE);
}
public void updateResourceClasses() {
execTask(
new UpdateResourceClassesTask(mAppModule != null && mAppModule.viewBindingEnabled));
}
public boolean isBuilding() {
return isBuilding;
}
public String typeString(int type) {
switch (type) {
case TASK_ASSEMBLE_DEBUG:
return getString(R.string.build_debug);
case TASK_ASSEMBLE_RELEASE:
return getString(R.string.build_release);
case TASK_BUILD:
return getString(R.string.build);
case TASK_CLEAN_BUILD:
return getString(R.string.clean_amp_build);
case TASK_CLEAN:
return getString(R.string.clean_project);
case TASK_BUNDLE:
return getString(R.string.create_aab);
case TASK_DEX:
return getString(R.string.compiling);
default:
return "";
}
}
}
@@ -1,83 +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.services.builder;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.ThreadUtils;
import com.itsaky.androidide.projects.IDEModule;
import com.itsaky.androidide.projects.IDEProject;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Objects;
import java.util.Optional;
class MainThreadBuildListener implements BuildListener {
private final BuildListener listener;
public MainThreadBuildListener(@NonNull final BuildListener listener) {
Objects.requireNonNull(listener, "Build listener cannot be null");
this.listener = listener;
}
@Override
public void onBuildModified() {
ThreadUtils.runOnUiThread(listener::onBuildModified);
}
@Override
public void onProjectLoaded(IDEProject project, Optional<IDEModule> appModule) {
ThreadUtils.runOnUiThread(() -> listener.onProjectLoaded(project, appModule));
}
@Override
public void onStartingGradleDaemon(GradleTask task) {
ThreadUtils.runOnUiThread(() -> listener.onStartingGradleDaemon(task));
}
@Override
public void onRunTask(GradleTask task, String name) {
ThreadUtils.runOnUiThread(() -> listener.onRunTask(task, name));
}
@Override
public void onBuildSuccessful(GradleTask task, String msg) {
ThreadUtils.runOnUiThread(() -> listener.onBuildSuccessful(task, msg));
}
@Override
public void onBuildFailed(GradleTask task, String msg) {
ThreadUtils.runOnUiThread(() -> listener.onBuildFailed(task, msg));
}
@Override
public void saveFiles() {
ThreadUtils.runOnUiThread(listener::saveFiles);
}
@Override
public void appendOutput(GradleTask task, CharSequence text) {
ThreadUtils.runOnUiThread(() -> listener.appendOutput(task, text));
}
@Override
public void prepareBuild() {
ThreadUtils.runOnUiThread(listener::prepareBuild);
}
}
@@ -1,46 +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.tasks;
import com.itsaky.androidide.services.builder.IDEService;
public abstract class BaseGradleTask implements GradleTask {
@Override
public boolean affectsGeneratedSources() {
return false;
}
@Override
public boolean shouldSaveFiles() {
return getType() == Type.BUILD
|| getType() == Type.LINT
|| getTaskID() == IDEService.TASK_DEX;
}
@Override
public boolean equals(Object obj) {
return obj != null
&& obj instanceof GradleTask
&& getTaskID() == ((GradleTask) obj).getTaskID()
&& getType() == ((GradleTask) obj).getType();
}
}
@@ -1,68 +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.tasks;
import java.util.List;
public interface GradleTask {
public String getName();
public String getCommands();
public List<String> getTasks();
public int getTaskID();
public boolean affectsGeneratedSources();
public boolean shouldSaveFiles();
public boolean canOutput();
public Type getType();
public static enum Type {
ANDROIDIDE_PLUGIN,
BUILD,
LINT,
HELP,
OTHER
}
public static final int TASK_SHOW_DEPENDENCIES = 0;
public static final int TASK_ASSEMBLE_DEBUG = 1;
public static final int TASK_ASSEMBLE_RELEASE = 2;
public static final int TASK_BUILD = 3;
public static final int TASK_BUNDLE = 4;
public static final int TASK_CLEAN = 5;
public static final int TASK_CLEAN_BUILD = 6;
public static final int TASK_COMPILE_JAVA = 7;
public static final int TASK_DEX = 8;
public static final int TASK_LINT = 9;
public static final int TASK_LINT_DEBUG = 10;
public static final int TASK_LINT_RELEASE = 11;
public static final int TASK_TASKS = 12;
public static final int TASK_INIT_PROJECT = 13;
public static final int TASK_PROCESS_DEBUG_RESOURCES = 14;
public static final int DATABINDING_GEN_BASE_CLASSES_DEBUG = 15;
public static final int TASK_UPDATE_RESOURCE_CLASSES = 16;
}
@@ -1,49 +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.tasks.gradle;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.gradle.build.AssembleDebug;
import com.itsaky.androidide.tasks.gradle.build.AssembleRelease;
import com.itsaky.androidide.tasks.gradle.build.Build;
import com.itsaky.androidide.tasks.gradle.build.Bundle;
import com.itsaky.androidide.tasks.gradle.build.Clean;
import com.itsaky.androidide.tasks.gradle.build.CleanBuild;
import com.itsaky.androidide.tasks.gradle.lint.Lint;
import com.itsaky.androidide.tasks.gradle.lint.LintDebug;
import com.itsaky.androidide.tasks.gradle.lint.LintRelease;
import com.itsaky.androidide.tasks.gradle.plugin.InitializeIDEProject;
import com.itsaky.androidide.tasks.gradle.resources.ProcessDebugResources;
public class BaseGradleTasks {
public static final BaseGradleTask BUILD = new Build(),
ASSEMBLE_DEBUG = new AssembleDebug(),
ASSEMBLE_RELEASE = new AssembleRelease(),
BUNDLE = new Bundle(),
CLEAN = new Clean(),
CLEAN_BUILD = new CleanBuild(),
LINT = new Lint(),
LINT_DEBUG = new LintDebug(),
LINT_RELEASE = new LintRelease(),
PROCESS_DEBUG_RESOURCES = new ProcessDebugResources(),
INITIALIZE_IDE_PROJECT = new InitializeIDEProject();
}
@@ -1,153 +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.tasks.gradle.build;
import static com.itsaky.androidide.models.ApkMetadata.ArtifactType;
import static com.itsaky.androidide.models.ApkMetadata.Element;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.models.ApkMetadata;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.utils.JSONUtility;
import com.itsaky.androidide.utils.ILogger;
import java.io.File;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public abstract class ApkGeneratingTask extends BaseGradleTask {
private static final ILogger LOG = ILogger.newInstance("ApkGeneratingTask");
/**
* Get the directories where APKs are generated. These directories must contain an
* output-metadata.json file
*/
protected abstract Set<File> provideApkDirectories(File buildDir);
protected Set<File> directories(File buildDir) {
if (buildDir == null || !buildDir.exists() || !buildDir.isDirectory()) {
return Collections.emptySet();
}
return provideApkDirectories(buildDir);
}
/** Get generated APKs */
public Set<File> getApks(File buildDir) {
final Set<File> result = new HashSet<>();
final Set<File> dirs = directories(buildDir);
if (dirs == null || dirs.isEmpty()) {
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)) {
LOG.error(
"No output-metadata.json file found in APK output directory"
+ " or the file is empty. APK directory:",
dir);
continue;
}
final String contents = FileIOUtils.readFile2String(jsonData);
try {
final ApkMetadata metadata = JSONUtility.gson.fromJson(contents, ApkMetadata.class);
if (!isValid(metadata)) {
LOG.warn("Invalid APK metadata:", metadata);
continue;
}
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("APK generated:", apk);
result.add(apk);
}
}
}
} catch (Throwable th) {
LOG.error(
StudioApp.getInstance()
.getString(
com.itsaky.androidide.R.string
.err_cannot_parse_apk_metadata),
th);
}
}
return result;
}
private 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;
}
}
@@ -1,69 +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.tasks.gradle.build;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import java.io.File;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class AssembleDebug extends ApkGeneratingTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.build_debug);
}
@Override
protected Set<File> provideApkDirectories(File buildDir) {
final Set<File> dirs = new HashSet<>();
dirs.add(new File(buildDir, "outputs/apk/debug"));
return dirs;
}
@Override
public String getCommands() {
return "assembleDebug";
}
@Override
public List<String> getTasks() {
return Collections.singletonList(getCommands());
}
@Override
public int getTaskID() {
return IDEService.TASK_ASSEMBLE_DEBUG;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public Type getType() {
return Type.BUILD;
}
}
@@ -1,72 +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.tasks.gradle.build;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.GradleTask;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class AssembleRelease extends ApkGeneratingTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.build_release);
}
@Override
protected Set<File> provideApkDirectories(File buildDir) {
final Set<File> dirs = new HashSet<>();
dirs.add(new File(buildDir, "outputs/apk/release"));
return dirs;
}
@Override
public String getCommands() {
return "assembleRelease";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return IDEService.TASK_ASSEMBLE_RELEASE;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return GradleTask.Type.BUILD;
}
}
@@ -1,73 +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.tasks.gradle.build;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.GradleTask;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Build extends ApkGeneratingTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.build);
}
@Override
protected Set<File> provideApkDirectories(File buildDir) {
final Set<File> dirs = new HashSet<>();
dirs.add(new File(buildDir, "outputs/apk/debug"));
dirs.add(new File(buildDir, "outputs/apk/release"));
return dirs;
}
@Override
public String getCommands() {
return "build";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return IDEService.TASK_BUILD;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return GradleTask.Type.BUILD;
}
}
@@ -1,63 +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.tasks.gradle.build;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Arrays;
import java.util.List;
public class Bundle extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.create_aab);
}
@Override
public String getCommands() {
return "bundle";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return IDEService.TASK_BUNDLE;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return GradleTask.Type.BUILD;
}
}
@@ -1,63 +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.tasks.gradle.build;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Arrays;
import java.util.List;
public class Clean extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.clean_project);
}
@Override
public String getCommands() {
return "clean";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return IDEService.TASK_CLEAN;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return GradleTask.Type.BUILD;
}
}
@@ -1,63 +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.tasks.gradle.build;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Arrays;
import java.util.List;
public class CleanBuild extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.clean_amp_build);
}
@Override
public String getCommands() {
return "clean build";
}
@Override
public List<String> getTasks() {
return Arrays.asList("clean", "build");
}
@Override
public int getTaskID() {
return IDEService.TASK_CLEAN_BUILD;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return GradleTask.Type.BUILD;
}
}
@@ -1,62 +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.tasks.gradle.lint;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import java.util.Arrays;
import java.util.List;
public class Lint extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.lint);
}
@Override
public String getCommands() {
return "lint";
}
@Override
public List<String> getTasks() {
return Arrays.asList("lint");
}
@Override
public int getTaskID() {
return IDEService.TASK_LINT;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public Type getType() {
return Type.LINT;
}
}
@@ -1,62 +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.tasks.gradle.lint;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import java.util.Collections;
import java.util.List;
public class LintDebug extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.lint_debug);
}
@Override
public String getCommands() {
return "lintDebug";
}
@Override
public List<String> getTasks() {
return Collections.singletonList("lintDebug");
}
@Override
public int getTaskID() {
return IDEService.TASK_LINT_DEBUG;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public Type getType() {
return Type.LINT;
}
}
@@ -1,62 +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.tasks.gradle.lint;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import java.util.Arrays;
import java.util.List;
public class LintRelease extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.lint_release);
}
@Override
public String getCommands() {
return "lintRelease";
}
@Override
public List<String> getTasks() {
return Arrays.asList("lintRelease");
}
@Override
public int getTaskID() {
return IDEService.TASK_LINT_RELEASE;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public Type getType() {
return Type.LINT;
}
}
@@ -1,60 +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.tasks.gradle.plugin;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Arrays;
import java.util.List;
public class InitializeIDEProject extends BaseGradleTask {
@Override
public String getName() {
return "initializeIDEProject";
}
@Override
public String getCommands() {
return "initializeIDEProject";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return TASK_INIT_PROJECT;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return Type.ANDROIDIDE_PLUGIN;
}
}
@@ -1,64 +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.tasks.gradle.resources;
import com.itsaky.androidide.tasks.BaseGradleTask;
import java.util.Arrays;
import java.util.List;
public class DatabindingGenBaseClassesDebug extends BaseGradleTask {
@Override
public boolean affectsGeneratedSources() {
return true;
}
@Override
public String getName() {
return "Generate databinding classes";
}
@Override
public String getCommands() {
return "databindingGenBaseClassesDebug";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return DATABINDING_GEN_BASE_CLASSES_DEBUG;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public Type getType() {
return Type.BUILD;
}
}
@@ -1,65 +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.tasks.gradle.resources;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Arrays;
import java.util.List;
public class ProcessDebugResources extends BaseGradleTask {
@Override
public boolean affectsGeneratedSources() {
return true;
}
@Override
public String getName() {
return "processDebugResources";
}
@Override
public String getCommands() {
return "processDebugResources";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return TASK_PROCESS_DEBUG_RESOURCES;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return Type.BUILD;
}
}
@@ -1,82 +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.tasks.gradle.resources;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.ArrayList;
import java.util.List;
public class UpdateResourceClassesTask extends BaseGradleTask {
private final boolean viewBinding;
public UpdateResourceClassesTask(boolean viewBinding) {
this.viewBinding = viewBinding;
}
@Override
public boolean affectsGeneratedSources() {
return true;
}
@Override
public String getName() {
return "Update resource classes";
}
@Override
public String getCommands() {
String command = "";
if (viewBinding) {
command = new DatabindingGenBaseClassesDebug().getCommands();
command += " ";
}
command += new ProcessDebugResources().getCommands();
return command;
}
@Override
public List<String> getTasks() {
List<String> tasks = new ArrayList<>();
if (viewBinding) {
tasks.addAll(new DatabindingGenBaseClassesDebug().getTasks());
}
tasks.addAll(new ProcessDebugResources().getTasks());
return tasks;
}
@Override
public int getTaskID() {
return TASK_UPDATE_RESOURCE_CLASSES;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return Type.BUILD;
}
}
@@ -37,9 +37,9 @@ package com.itsaky.androidide.utils
import android.content.Context
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.actions.build.CancelBuildAction
import com.itsaky.androidide.actions.build.QuickRunAction
import com.itsaky.androidide.actions.build.RunActionMenu
import com.itsaky.androidide.actions.build.CancelBuildAction
import com.itsaky.androidide.actions.editor.CopyAction
import com.itsaky.androidide.actions.editor.CutAction
import com.itsaky.androidide.actions.editor.ExpandSelectionAction
@@ -1,201 +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.utils;
import android.text.TextUtils;
import com.blankj.utilcode.util.FileIOUtils;
import com.itsaky.androidide.projects.AndroidProject;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileFilter;
import java.util.regex.Pattern;
import fr.arnaudguyon.xmltojsonlib.XmlToJson;
public class ProjectFinder {
public static AndroidProject fromFolder(File file) {
if (!file.isDirectory() || file.isHidden()) return null;
File settings = new File(file, "settings.gradle");
if (!settings.exists()) return null;
String content = FileUtil.readFile(settings.getAbsolutePath());
if (TextUtils.isEmpty(content)) return null;
AndroidProject project = new AndroidProject();
for (File module : file.listFiles()) {
if (module.isDirectory() && !module.isHidden()) {
try {
proceedModule(file, module, project);
} catch (JSONException e) {
continue;
}
}
}
project.setAppName(file.getName());
project.setProjectPath(file.getAbsolutePath());
if (project.getMainModule() == null
&& project.getModulePaths().size() > 0
&& project.getModulePaths().get(0) != null) {
File f = new File(project.getModulePaths().get(0));
if (!f.exists() || !f.isDirectory()) return null;
project.setMainModule(f.getName());
project.setMainSourcePath(new File(f, "src/main/java").getAbsolutePath());
}
return project;
}
private static void proceedModule(File file, File module, AndroidProject project)
throws JSONException {
final Pattern appPattern =
Pattern.compile("apply\\s+plugin\\s*:\\s*(['\"])com.android.application(['\"])");
File projectGradle = new File(file, "build.gradle");
File moduleGradle = new File(module, "build.gradle");
File manifest = new File(module, "src/main/AndroidManifest.xml");
File res = new File(module, "src/main/res");
if (!module.exists()
|| !projectGradle.exists()
|| !moduleGradle.exists()
|| !manifest.exists()) return;
if (!module.isDirectory()
|| !moduleGradle.isFile()
|| !projectGradle.isFile()
|| !manifest.isFile()) return;
File java = new File(module, "src/main/java");
if (java.exists() && java.isDirectory())
project.addSource(java.getAbsolutePath()).addModule(module.getAbsolutePath());
File libsFolder = new File(module, "libs");
if (libsFolder.exists() && libsFolder.isDirectory()) {
File[] libs = libsFolder.listFiles();
if (libs != null && libs.length > 0) {
for (File lib : libs)
if (lib.getName().endsWith(".jar")) project.addClasspath(lib.getAbsolutePath());
}
}
project.addSource(
new File(module, "build/generated/source/buildConfig/debug").getAbsolutePath());
project.addSource(
new File(module, "build/generated/data_binding_base_class_source_out/debug/out")
.getAbsolutePath());
JSONObject obj =
new XmlToJson.Builder(FileUtil.readFile(manifest.getAbsolutePath()))
.build()
.toJson();
if (!obj.has("manifest")) return;
String iconPath = null;
if (obj.has("manifest")
&& obj.getJSONObject("manifest").has("application")
&& obj.getJSONObject("manifest").getJSONObject("application").has("android:icon")) {
String iconRes =
obj.getJSONObject("manifest")
.getJSONObject("application")
.getString("android:icon");
if (iconRes.startsWith("@drawable")) {
final String iconName = iconRes.replace("@drawable/", "");
File[] drawables = listDrawables(res);
for (File drawable : drawables) {
File[] icons = listFilesNotEndingWith(drawable, ".xml");
for (File icon : icons) {
String nameWithoutExtension =
icon.getName().substring(0, icon.getName().lastIndexOf("."));
if (nameWithoutExtension.equals(iconName)) {
iconPath = icon.getAbsolutePath();
break;
}
}
}
} else if (iconRes.startsWith("@drawable")) {
final String iconName = iconRes.replace("@drawable/", "");
File[] drawables = listMipmaps(res);
for (File drawable : drawables) {
File[] icons = listFilesNotEndingWith(drawable, ".xml");
for (File icon : icons) {
String nameWithoutExtension =
icon.getName().substring(0, icon.getName().lastIndexOf("."));
if (nameWithoutExtension.equals(iconName)) {
iconPath = icon.getAbsolutePath();
break;
}
}
}
}
}
final String packageName = obj.getJSONObject("manifest").getString("package");
final String mainModuleName = module.getName();
boolean isMainModule = false;
if (project.getMainModule() == null) {
String content = FileIOUtils.readFile2String(moduleGradle);
if (content != null && appPattern.matcher(content).find()) {
isMainModule = true;
project.setMainModule(mainModuleName);
project.setMainSourcePath(new File(module, "src/main/java").getAbsolutePath());
}
}
if (iconPath != null) {
project.setIconPath(iconPath);
}
if (packageName != null && isMainModule) {
project.setPackageName(packageName);
}
}
private static File[] listDrawables(File file) {
return listFolderStartingWith(file, "drawable");
}
private static File[] listMipmaps(File file) {
return listFolderStartingWith(file, "mipmap");
}
private static File[] listFolderStartingWith(File file, final String prefix) {
return file.listFiles(
new FileFilter() {
@Override
public boolean accept(File p1) {
return p1.isDirectory() && p1.getName().startsWith(prefix);
}
});
}
private static File[] listFilesNotEndingWith(File file, final String suffix) {
return file.listFiles(
new FileFilter() {
@Override
public boolean accept(File p1) {
return p1.isFile() && !p1.getName().endsWith(suffix);
}
});
}
}
@@ -25,9 +25,6 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;
import com.itsaky.androidide.projects.AndroidProject;
import com.itsaky.androidide.projects.IDEProject;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
@@ -37,8 +34,6 @@ import java.util.Objects;
public class EditorViewModel extends ViewModel {
private final MutableLiveData<List<File>> mFiles = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<AndroidProject> mProject = new MutableLiveData<>(null);
private final MutableLiveData<IDEProject> mIDEProject = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> mFilesModified = new MutableLiveData<>(false);
/**
@@ -47,44 +42,6 @@ public class EditorViewModel extends ViewModel {
*/
private final MutableLiveData<Pair<Integer, File>> mCurrentFile = new MutableLiveData<>(null);
/**
* Get the current {@link AndroidProject}.
*
* @return The android project.
*/
@Nullable
public AndroidProject getAndroidProject() {
return mProject.getValue();
}
/**
* Set the android project currently opened in the editor activity.
*
* @param project The project that is opened.
*/
public void setAndroidProject(final AndroidProject project) {
this.mProject.setValue(project);
}
/**
* Get the {@link IDEProject} opened in the editor activity.
*
* @return The ide project.
*/
@Nullable
public IDEProject getIDEProject() {
return this.mIDEProject.getValue();
}
/**
* Set the IDEProject that is currently opened in the editor activity.
*
* @param project The project that is opened.
*/
public void setIDEProject(final IDEProject project) {
this.mIDEProject.setValue(project);
}
/**
* Add the given file to the list of opened files.
*
@@ -112,6 +69,10 @@ public class EditorViewModel extends ViewModel {
setCurrentFile(-1, null);
}
public void setCurrentFile(final int index, @Nullable final File file) {
mCurrentFile.setValue(Pair.create(index, file));
}
/**
* Get the opened file at the given index.
*
@@ -167,10 +128,6 @@ public class EditorViewModel extends ViewModel {
return mCurrentFile.getValue().second;
}
public void setCurrentFile(final int index, @Nullable final File file) {
mCurrentFile.setValue(Pair.create(index, file));
}
public void setFilesModified(boolean modified) {
this.mFilesModified.setValue(modified);
}
@@ -62,11 +62,6 @@ import com.itsaky.androidide.utils.TypefaceUtils;
import com.itsaky.inflater.values.ValuesTableFactory;
import com.itsaky.lsp.models.Range;
import io.github.rosemoe.sora.event.ContentChangeEvent;
import io.github.rosemoe.sora.lang.EmptyLanguage;
import io.github.rosemoe.sora.text.Content;
import io.github.rosemoe.sora.widget.component.Magnifier;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.Token;
@@ -78,6 +73,11 @@ import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import io.github.rosemoe.sora.event.ContentChangeEvent;
import io.github.rosemoe.sora.lang.EmptyLanguage;
import io.github.rosemoe.sora.text.Content;
import io.github.rosemoe.sora.widget.component.Magnifier;
/**
* A view that handles opened code editors.
*