diff --git a/.idea/misc.xml b/.idea/misc.xml
index 06ac8f54d..af8abee99 100755
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -86,6 +86,8 @@
+
+
diff --git a/app/build.gradle b/app/build.gradle
index cc8f88dfb..ff62df5d9 100755
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -109,6 +109,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Local projects here
+ implementation project (':build-tools:builder-api')
implementation project (':treeview')
implementation project (':emulatorview')
implementation project (':xml-inflater')
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 31aedb00f..1c4086af3 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -68,6 +68,9 @@
android:configChanges="orientation|screenSize|uiMode"
android:windowSoftInputMode="adjustResize" />
+
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/assets/data/common/tooling-api-all.jar b/app/src/main/assets/data/common/tooling-api-all.jar
index eff3804be..40c3d54eb 100644
Binary files a/app/src/main/assets/data/common/tooling-api-all.jar and b/app/src/main/assets/data/common/tooling-api-all.jar differ
diff --git a/app/src/main/java/com/itsaky/androidide/EditorActivity.java b/app/src/main/java/com/itsaky/androidide/EditorActivity.java
index 8a95d0a5b..02d406075 100755
--- a/app/src/main/java/com/itsaky/androidide/EditorActivity.java
+++ b/app/src/main/java/com/itsaky/androidide/EditorActivity.java
@@ -21,12 +21,15 @@ package com.itsaky.androidide;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.ServiceConnection;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.os.Handler;
+import android.os.IBinder;
import android.os.Looper;
import android.text.TextUtils;
import android.view.Gravity;
@@ -97,6 +100,7 @@ import com.itsaky.androidide.models.SearchResult;
import com.itsaky.androidide.models.SheetOption;
import com.itsaky.androidide.project.AndroidProject;
import com.itsaky.androidide.project.IDEProject;
+import com.itsaky.androidide.services.GradleBuildService;
import com.itsaky.androidide.services.LogReceiver;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.shell.ShellServer;
@@ -105,8 +109,9 @@ import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.EditorActivityActions;
import com.itsaky.androidide.utils.EditorBottomSheetBehavior;
import com.itsaky.androidide.utils.Environment;
-import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.utils.ILogger;
+import com.itsaky.androidide.utils.JSONUtility;
+import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.utils.RecursiveFileSearcher;
import com.itsaky.androidide.utils.Symbols;
import com.itsaky.androidide.viewmodel.EditorViewModel;
@@ -179,11 +184,808 @@ public class EditorActivity extends StudioActivity
private EditorBottomSheetBehavior extends View> mEditorBottomSheet;
private EditorViewModel mViewModel;
+ private GradleBuildService mBuildService;
+ private final ServiceConnection mGradleServiceConnection =
+ new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mBuildService = ((GradleBuildService.GradleServiceBinder) service).getService();
+ LOG.info("Gradle build service has been started...");
+
+ mBuildService.startToolingServer(() -> initializeProject());
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mBuildService = null;
+ LOG.info("Disconnected from Gradle build service...");
+ }
+ };
+
@Override
- protected View bindLayout() {
- mBinding = ActivityEditorBinding.inflate(getLayoutInflater());
- mDiagnosticInfoBinding = mBinding.diagnosticInfo;
- return mBinding.getRoot();
+ public void onBackPressed() {
+ if (mBinding.getRoot().isDrawerOpen(GravityCompat.END)) {
+ mBinding.getRoot().closeDrawer(GravityCompat.END);
+ } else if (mBinding.getRoot().isDrawerOpen(GravityCompat.START)) {
+ mBinding.getRoot().closeDrawer(GravityCompat.START);
+ } else if (getDaemonStatusFragment().isShowing()) {
+ getDaemonStatusFragment().dismiss();
+ } else if (mFileOptionsFragment != null && mFileOptionsFragment.isShowing()) {
+ mFileOptionsFragment.dismiss();
+ } else if (mEditorBottomSheet.getState() == BottomSheetBehavior.STATE_EXPANDED) {
+ mEditorBottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ } else {
+ confirmProjectClose();
+ }
+ }
+
+ @SuppressLint("RestrictedApi")
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (menu instanceof MenuBuilder) {
+ MenuBuilder builder = (MenuBuilder) menu;
+ builder.setOptionalIconsVisible(true);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ ensureToolbarMenu(menu);
+ return true;
+ }
+
+ public void ensureToolbarMenu(@NonNull Menu menu) {
+ menu.clear();
+
+ final var data = new ActionData();
+ final var currentEditor = getCurrentEditor();
+ data.put(Context.class, EditorActivity.this);
+ data.put(CodeEditorView.class, currentEditor);
+
+ if (currentEditor != null) {
+ data.put(IDEEditor.class, currentEditor.getEditor());
+ data.put(File.class, currentEditor.getEditor().getFile());
+ }
+
+ ActionsRegistry.getInstance()
+ .fillMenu(
+ data,
+ com.itsaky.androidide.actions.ActionItem.Location.EDITOR_TOOLBAR,
+ menu);
+ }
+
+ @Nullable
+ public CodeEditorView getCurrentEditor() {
+ if (mViewModel.getCurrentFileIndex() != -1) {
+ return getEditorAtIndex(mViewModel.getCurrentFileIndex());
+ }
+
+ return null;
+ }
+
+ @Nullable
+ public CodeEditorView getEditorAtIndex(final int index) {
+ return (CodeEditorView) mBinding.editorContainer.getChildAt(index);
+ }
+
+ @Override
+ public EditorActivity provide() {
+ return this;
+ }
+
+ @Override
+ public EditorActivity provideEditorActivity() {
+ 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();
+ }
+
+ public void setIDEProject(IDEProject project) {
+ if (mViewModel != null) {
+ mViewModel.setIDEProject(project);
+ }
+ }
+
+ @Nullable
+ private AndroidProject getAndroidProject() {
+ if (mViewModel == null) {
+ return null;
+ }
+
+ return mViewModel.getAndroidProject();
+ }
+
+ public void handleSearchResults(Map> results) {
+ setSearchResultAdapter(
+ new com.itsaky.androidide.adapters.SearchListAdapter(
+ results,
+ file -> {
+ openFile(file);
+ hideViewOptions();
+ },
+ match -> {
+ openFileAndSelect(match.file, match);
+ hideViewOptions();
+ }));
+
+ showSearchResults();
+
+ if (mSearchingProgress != null && mSearchingProgress.isShowing()) {
+ mSearchingProgress.dismiss();
+ }
+ }
+
+ public void appendApkLog(LogLine line) {
+ final var logFragment = bottomSheetTabAdapter.getLogFragment();
+ if (logFragment != null) {
+ logFragment.appendLog(line);
+ }
+ }
+
+ public void appendBuildOut(final String str) {
+ final var frag = bottomSheetTabAdapter.getBuildOutputFragment();
+
+ if (frag != null) {
+ frag.appendOutput(str);
+ }
+ }
+
+ public void showDaemonStatus() {
+ ShellServer shell = getApp().newShell(t -> getDaemonStatusFragment().append(t));
+ shell.bgAppend(String.format("echo '%s'", getString(R.string.msg_getting_daemom_status)));
+ shell.bgAppend(
+ String.format(
+ "cd '%s' && sh gradlew --status",
+ Objects.requireNonNull(getAndroidProject()).getProjectPath()));
+ if (!getDaemonStatusFragment().isShowing()) {
+ getDaemonStatusFragment().show(getSupportFragmentManager(), "daemon_status");
+ }
+ }
+
+ public TextSheetFragment getDaemonStatusFragment() {
+ return mDaemonStatusFragment == null
+ ? mDaemonStatusFragment =
+ new TextSheetFragment()
+ .setTextSelectable(true)
+ .setTitleText(R.string.gradle_daemon_status)
+ : mDaemonStatusFragment;
+ }
+
+ public void showSearchResults() {
+ if (mEditorBottomSheet.getState() != BottomSheetBehavior.STATE_EXPANDED) {
+ mEditorBottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED);
+ }
+
+ final int index =
+ bottomSheetTabAdapter.findIndexOfFragmentByClass(SearchResultFragment.class);
+ if (index >= 0 && index < mBinding.bottomSheet.tabs.getTabCount()) {
+ final var tab = mBinding.bottomSheet.tabs.getTabAt(index);
+ if (tab != null) {
+ tab.select();
+ }
+ }
+ }
+
+ public void handleDiagnosticsResultVisibility(boolean errorVisible) {
+ runOnUiThread(
+ () -> {
+ final var diagnosticsFragment = bottomSheetTabAdapter.getDiagnosticsFragment();
+ if (diagnosticsFragment != null) {
+ diagnosticsFragment.handleResultVisibility(errorVisible);
+ }
+ });
+ }
+
+ public void handleSearchResultVisibility(boolean errorVisible) {
+ runOnUiThread(
+ () -> {
+ final var searchResultFragment =
+ bottomSheetTabAdapter.getSearchResultFragment();
+ if (searchResultFragment != null) {
+ searchResultFragment.handleResultVisibility(errorVisible);
+ }
+ });
+ }
+
+ public void showFirstBuildNotice() {
+ DialogUtils.newMaterialDialogBuilder(this)
+ .setPositiveButton(android.R.string.ok, null)
+ .setTitle(R.string.title_first_build)
+ .setMessage(R.string.msg_first_build)
+ .setCancelable(false)
+ .create()
+ .show();
+ }
+
+ @Override
+ public void onGroupClick(DiagnosticGroup group) {
+ if (group != null
+ && group.file != null
+ && group.file.exists()
+ && FileUtils.isUtf8(group.file)) {
+ openFile(group.file);
+ hideViewOptions();
+ }
+ }
+
+ @Override
+ public void onDiagnosticClick(File file, @NonNull DiagnosticItem diagnostic) {
+ openFileAndSelect(file, diagnostic.getRange());
+ hideViewOptions();
+ }
+
+ public void openFileAndSelect(File file, Range selection) {
+ openFile(file, selection);
+ final var opened = getEditorForFile(file);
+ if (opened != null && opened.getEditor() != null) {
+ final var editor = opened.getEditor();
+ final var range = editor.validateRange(selection);
+ editor.post(
+ () -> {
+ if (LSPUtils.isEqual(range.getStart(), range.getEnd())) {
+ editor.setSelection(
+ range.getStart().getLine(), range.getEnd().getColumn());
+ } else {
+ editor.setSelection(range);
+ }
+ });
+ }
+ }
+
+ @Nullable
+ public CodeEditorView getEditorForFile(@NonNull final File file) {
+ for (int i = 0; i < mViewModel.getOpenedFileCount(); i++) {
+ final CodeEditorView editor = (CodeEditorView) mBinding.editorContainer.getChildAt(i);
+ if (file.equals(editor.getFile())) {
+ return editor;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public CodeEditorView openFile(File file) {
+ return openFile(file, null);
+ }
+
+ public void hideViewOptions() {
+ if (mEditorBottomSheet.getState() != BottomSheetBehavior.STATE_COLLAPSED) {
+ mEditorBottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ }
+ }
+
+ public CodeEditorView openFile(File file, com.itsaky.lsp.models.Range selection) {
+ if (selection == null) {
+ selection = com.itsaky.lsp.models.Range.NONE;
+ }
+
+ int index = openFileAndGetIndex(file, selection);
+ final var tab = mBinding.tabs.getTabAt(index);
+ if (tab != null && index >= 0 && !tab.isSelected()) {
+ tab.select();
+ }
+
+ if (mBinding.editorDrawerLayout.isDrawerOpen(GravityCompat.END)) {
+ mBinding.editorDrawerLayout.closeDrawer(GravityCompat.END);
+ }
+
+ mBinding.editorContainer.setDisplayedChild(index);
+
+ try {
+ return getEditorAtIndex(index);
+ } catch (Throwable th) {
+ LOG.error("Unable to get editor fragment at opened file index", index);
+ LOG.error(th);
+ return null;
+ }
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ public int openFileAndGetIndex(File file, com.itsaky.lsp.models.Range selection) {
+ final var openedFileIndex = findIndexOfEditorByFile(file);
+
+ if (openedFileIndex != -1) {
+ LOG.error("File is already opened. File: " + file);
+ return openedFileIndex;
+ }
+
+ final var position = mViewModel.getOpenedFileCount();
+
+ LOG.info("Opening file at index:", position, "file: ", file);
+
+ final var editor = new CodeEditorView(this, file, selection);
+ editor.getEditor().subscribeEvent(ContentChangeEvent.class, this::onEditorContentChanged);
+ editor.setLayoutParams(
+ new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
+
+ mBinding.editorContainer.addView(editor);
+ mBinding.editorContainer.setDisplayedChild(position);
+ mBinding.tabs.addTab(mBinding.tabs.newTab().setText(file.getName()));
+
+ mViewModel.addFile(file);
+ mViewModel.setCurrentFile(position, file);
+
+ return position;
+ }
+
+ public int findIndexOfEditorByFile(File file) {
+ if (file == null) {
+ LOG.error("Cannot find index of a null file.");
+ return -1;
+ }
+
+ for (int i = 0; i < mViewModel.getOpenedFileCount(); i++) {
+ final var opened = mViewModel.getOpenedFile(i);
+ if (opened.equals(file)) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private void onEditorContentChanged(ContentChangeEvent event, Unsubscribe unsubscribe) {
+ if (event.getAction() != ContentChangeEvent.ACTION_SET_NEW_TEXT) {
+ mViewModel.setFilesModified(true);
+ }
+ }
+
+ @Override
+ public void showFileOptions(File thisFile, TreeNode node) {
+ mLastHeld = node;
+ getFileOptionsFragment(thisFile)
+ .show(getSupportFragmentManager(), TAG_FILE_OPTIONS_FRAGMENT);
+ }
+
+ public OptionsListFragment getFileOptionsFragment(File file) {
+ mFileOptionsFragment = new OptionsListFragment();
+ mFileOptionsFragment.addOption(
+ new SheetOption(0, R.drawable.ic_file_copy_path, R.string.copy_path, file));
+ mFileOptionsFragment.addOption(
+ new SheetOption(1, R.drawable.ic_file_rename, R.string.rename_file, file));
+ mFileOptionsFragment.addOption(
+ new SheetOption(2, R.drawable.ic_delete, R.string.delete_file, file));
+ if (file.isDirectory()) {
+ mFileOptionsFragment.addOption(
+ new SheetOption(3, R.drawable.ic_new_file, R.string.new_file, file));
+ mFileOptionsFragment.addOption(
+ new SheetOption(4, R.drawable.ic_new_folder, R.string.new_folder, file));
+ }
+
+ return mFileOptionsFragment;
+ }
+
+ public ActivityEditorBinding getBinding() {
+ return mBinding;
+ }
+
+ public LayoutDiagnosticInfoBinding getDiagnosticBinding() {
+ return mDiagnosticInfoBinding;
+ }
+
+ @Override
+ public void onTabSelected(@NonNull TabLayout.Tab tab) {
+ final var position = tab.getPosition();
+ mBinding.editorContainer.setDisplayedChild(position);
+
+ final var editorView = getEditorAtIndex(position);
+ Objects.requireNonNull(editorView);
+
+ editorView.onEditorSelected();
+ mViewModel.setCurrentFile(position, editorView.getFile());
+ refreshSymbolInput(editorView);
+ }
+
+ @Override
+ public void onTabUnselected(@NonNull TabLayout.Tab tab) {
+ final var position = tab.getPosition();
+ final var editorView = getEditorAtIndex(position);
+ if (editorView != null) {
+ final var editor = editorView.getEditor();
+ if (editor != null) {
+ editor.ensureWindowsDismissed();
+ }
+ }
+ }
+
+ @Override
+ public void onTabReselected(@NonNull TabLayout.Tab tab) {
+ mTabCloseAction.show(tab.view);
+ }
+
+ private void refreshSymbolInput(@NonNull CodeEditorView frag) {
+ symbolInput.bindEditor(frag.getEditor());
+ symbolInput.setSymbols(Symbols.forFile(frag.getFile()));
+ }
+
+ @Override
+ public boolean onNavigationItemSelected(@NonNull MenuItem p1) {
+ final int id = p1.getItemId();
+ if (id == R.id.editornav_discuss) {
+ getApp().openTelegramGroup();
+ } else if (id == R.id.editornav_suggest) {
+ getApp().openGitHub();
+ } else if (id == R.id.editornav_needHelp) {
+ showNeedHelpDialog();
+ } else if (id == R.id.editornav_settings) {
+ startActivity(new Intent(this, PreferencesActivity.class));
+ } else if (id == R.id.editornav_share) {
+ startActivity(IntentUtils.getShareTextIntent(getString(R.string.msg_share_app)));
+ } else if (id == R.id.editornav_close_project) {
+ confirmProjectClose();
+ } else if (id == R.id.editornav_terminal) {
+ openTerminal();
+ }
+ mBinding.getRoot().closeDrawer(GravityCompat.START);
+ return false;
+ }
+
+ public void loadFragment(Fragment fragment) {
+ super.loadFragment(fragment, mBinding.editorFrameLayout.getId());
+ }
+
+ public FileTreeFragment getFileTreeFragment() {
+ return mFileTreeFragment;
+ }
+
+ public TreeNode getLastHoldTreeNode() {
+ return mLastHeld;
+ }
+
+ public ProgressSheet getProgressSheet(int msg) {
+ if (mSearchingProgress != null && mSearchingProgress.isShowing()) {
+ mSearchingProgress.dismiss();
+ }
+ mSearchingProgress = new ProgressSheet();
+ mSearchingProgress.setCancelable(false);
+ mSearchingProgress.setWelcomeTextEnabled(false);
+ mSearchingProgress.setMessage(getString(msg));
+ mSearchingProgress.setSubMessageEnabled(false);
+ return mSearchingProgress;
+ }
+
+ public void setDiagnosticsAdapter(@NonNull final DiagnosticsAdapter adapter) {
+ runOnUiThread(
+ () -> {
+ final var diagnosticsFragment = bottomSheetTabAdapter.getDiagnosticsFragment();
+ if (diagnosticsFragment != null) {
+ diagnosticsFragment.setAdapter(adapter);
+ }
+ });
+ }
+
+ public void setSearchResultAdapter(@NonNull final SearchListAdapter adapter) {
+ runOnUiThread(
+ () -> {
+ final var searchResultFragment =
+ bottomSheetTabAdapter.getSearchResultFragment();
+ if (searchResultFragment != null) {
+ searchResultFragment.setAdapter(adapter);
+ }
+ });
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ public boolean saveAll() {
+ return saveAll(true);
+ }
+
+ public void install(@NonNull File apk) {
+ LOG.debug("Installing APK:", apk);
+ if (apk.exists()) {
+ Intent i = IntentUtils.getInstallAppIntent(apk);
+ if (i != null) {
+ startActivity(i);
+ } else {
+ getApp().toast(R.string.msg_apk_install_intent_failed, Toaster.Type.ERROR);
+ }
+ } else {
+ LOG.error("APK file does not exist!");
+ }
+ }
+
+ public void updateServices() {
+ new TaskExecutor()
+ .executeAsync(
+ () -> {
+ List cps =
+ Objects.requireNonNull(getAndroidProject()).getClassPaths();
+ getApp().getJavaLanguageServer()
+ .configurationChanged(
+ new JavaServerConfiguration(
+ cps.stream()
+ .map(Paths::get)
+ .collect(Collectors.toSet())));
+ return null;
+ },
+ __ -> setStatus(getString(R.string.msg_service_started)));
+ }
+
+ public void setStatus(final CharSequence text) {
+ try {
+ runOnUiThread(() -> mBinding.bottomSheet.statusText.setText(text));
+ } catch (Throwable th) {
+ LOG.error("Failed to update status text", th);
+ }
+ }
+
+ public void closeFile(int index) {
+ if (index >= 0 && index < mViewModel.getOpenedFileCount()) {
+ var opened = mViewModel.getOpenedFile(index);
+ LOG.info("Closing file:", opened);
+ final var editor = getEditorAtIndex(index);
+
+ if (editor != null && editor.isModified()) {
+ notifyFilesUnsaved(Collections.singletonList(editor), () -> closeFile(index));
+ return;
+ }
+
+ if (editor != null && editor.getEditor() != null) {
+ editor.getEditor().close();
+ } else {
+ LOG.error("Cannot save file before close. Editor instance is null");
+ }
+
+ mViewModel.removeFile(index);
+ mBinding.tabs.removeTabAt(index);
+ mBinding.editorContainer.removeViewAt(index);
+ } else {
+ LOG.error("Invalid file index. Cannot close.");
+ return;
+ }
+
+ mBinding.tabs.requestLayout();
+ }
+
+ public void closeAll() {
+ closeAll(null);
+ }
+
+ /**
+ * Close all opened files. If all the files are saved successfully, then the provided {@link
+ * Runnable} will be called.
+ *
+ * @param onSaved The {@link Runnable} to run when all the files are saved.
+ */
+ public void closeAll(Runnable onSaved) {
+ final var count = mViewModel.getOpenedFileCount();
+ final var unsavedFiles =
+ mViewModel.getOpenedFiles().stream()
+ .map(this::getEditorForFile)
+ .filter(editor -> editor != null && editor.isModified())
+ .collect(Collectors.toList());
+
+ if (unsavedFiles.isEmpty()) {
+ // Files were already saved, close all files one by one
+ for (int i = 0; i < count; i++) {
+ final var editor = getEditorAtIndex(i);
+ if (editor != null && editor.getEditor() != null) {
+ editor.getEditor().close();
+ } else {
+ LOG.error("Unable to close file at index:", i);
+ }
+ }
+
+ mViewModel.removeAllFiles();
+ mBinding.tabs.removeAllTabs();
+ mBinding.tabs.requestLayout();
+ mBinding.editorContainer.removeAllViews();
+
+ if (onSaved != null) {
+ onSaved.run();
+ }
+ } else {
+ // There are unsaved files
+ notifyFilesUnsaved(unsavedFiles, () -> closeAll(onSaved));
+ }
+ }
+
+ public boolean areFilesModified() {
+ return mViewModel.areFilesModified();
+ }
+
+ public EditorViewModel getViewModel() {
+ return mViewModel;
+ }
+
+ public void closeOthers() {
+ final var unsavedFiles =
+ mViewModel.getOpenedFiles().stream()
+ .map(this::getEditorForFile)
+ .filter(editor -> editor != null && editor.isModified())
+ .collect(Collectors.toList());
+
+ if (unsavedFiles.isEmpty()) {
+ final var file = mViewModel.getCurrentFile();
+ for (int i = 0; i < mViewModel.getOpenedFileCount(); i++) {
+ final var editor = getEditorAtIndex(i);
+
+ // Index of files changes as we keep close files
+ // So we compare the files instead of index
+ if (editor != null) {
+ if (!file.equals(editor.getFile())) {
+ closeFile(i);
+ }
+ } else {
+ LOG.error("Unable to save file at index:", i);
+ }
+ }
+ } else {
+ notifyFilesUnsaved(unsavedFiles, this::closeOthers);
+ }
+ }
+
+ public void previewLayout() {
+ try {
+
+ if (getCurrentEditor() == null || getCurrentEditor().getFile() == null) {
+ LOG.error("No file is opened. Cannot preview layout.");
+ return;
+ }
+
+ saveAll(false);
+
+ final Intent intent = new Intent(this, DesignerActivity.class);
+ intent.putExtra(
+ DesignerActivity.KEY_LAYOUT_PATH,
+ getCurrentEditor().getFile().getAbsolutePath());
+ intent.putStringArrayListExtra(DesignerActivity.KEY_RES_DIRS, getResourceDirectories());
+ LOG.info("Launching UI Designer...");
+ mUIDesignerLauncher.launch(intent);
+ } catch (Throwable th) {
+ LOG.error(getString(R.string.err_cannot_preview_layout), th);
+ getApp().toast(R.string.msg_cannot_preview_layout, Toaster.Type.ERROR);
+ }
+ }
+
+ public boolean saveAll(boolean notify) {
+ return saveAll(notify, false);
+ }
+
+ @NonNull
+ private ArrayList getResourceDirectories() {
+ final var dirs = new ArrayList();
+ 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());
+ }
+ }
+ }
+ }
+ return dirs;
+ }
+
+ public boolean saveAll(boolean notify, boolean canProcessResources) {
+ SaveResult result = saveAllResult();
+
+ if (notify) {
+ getApp().toast(R.string.all_saved, Toaster.Type.SUCCESS);
+ }
+
+ if (result.gradleSaved) {
+ notifySyncNeeded();
+ }
+
+ if (result.xmlSaved && canProcessResources && getBuildService() != null) {
+ getBuildService().updateResourceClasses();
+ }
+
+ return result.gradleSaved;
+ }
+
+ public SaveResult saveAllResult() {
+ SaveResult result = new SaveResult();
+ for (int i = 0; i < mViewModel.getOpenedFileCount(); i++) {
+ saveResult(i, result);
+ }
+
+ return result;
+ }
+
+ private void notifySyncNeeded() {
+ if (getBuildService() != null && !getBuildService().isBuilding()) {
+ getSyncBanner()
+ .setNegative(android.R.string.cancel, null)
+ .setPositive(
+ android.R.string.ok, v -> getBuildServiceHandler().assembleDebug(false))
+ .show();
+ }
+ }
+
+ private IDEService getBuildService() {
+ return mBuildServiceHandler.getService();
+ }
+
+ public void saveResult(int index, SaveResult result) {
+ if (index >= 0 && index < mViewModel.getOpenedFileCount()) {
+ var frag = getEditorAtIndex(index);
+ if (frag == null || frag.getFile() == null) {
+ return;
+ }
+
+ // Must be called before frag.save()
+ // Otherwise, it'll always return false
+ final boolean modified = frag.isModified();
+
+ frag.save();
+
+ final boolean isGradle = frag.getFile().getName().endsWith(".gradle");
+ final boolean isXml = frag.getFile().getName().endsWith(".xml");
+
+ if (!result.gradleSaved) {
+ result.gradleSaved = modified && isGradle;
+ }
+
+ if (!result.xmlSaved) {
+ result.xmlSaved = modified && isXml;
+ }
+ }
+
+ var modified = false;
+ for (var file : mViewModel.getOpenedFiles()) {
+ var editor = getEditorForFile(file);
+ if (editor == null) {
+ continue;
+ }
+ modified = modified || editor.isModified();
+ }
+
+ final boolean finalModified = modified;
+ ThreadUtils.runOnUiThread(() -> mViewModel.setFilesModified(finalModified));
+ }
+
+ public MaterialBanner getSyncBanner() {
+ return mBinding.syncBanner
+ .setContentTextColor(ContextCompat.getColor(this, R.color.primaryTextColor))
+ .setBannerBackgroundColor(ContextCompat.getColor(this, R.color.primaryLightColor))
+ .setButtonTextColor(ContextCompat.getColor(this, R.color.secondaryColor))
+ .setIcon(R.drawable.ic_sync)
+ .setContentText(R.string.msg_sync_needed);
+ }
+
+ public BuildServiceHandler getBuildServiceHandler() {
+ return mBuildServiceHandler;
+ }
+
+ /////////////////////////////////////////////////
+ ////////////// PRIVATE APIS /////////////////////
+ /////////////////////////////////////////////////
+
+ public AlertDialog getFindInProjectDialog() {
+ return mFindInProjectDialog == null ? createFindInProjectDialog() : mFindInProjectDialog;
+ }
+
+ @Override
+ public void onOptionsClick(SheetOption option) {
+ if (mFileOptionsHandler != null) {
+ mFileOptionsHandler.onOptionsClick(option);
+ }
}
@Override
@@ -226,6 +1028,186 @@ public class EditorActivity extends StudioActivity
EditorActivityActions.register(this);
}
+ @Override
+ protected View bindLayout() {
+ mBinding = ActivityEditorBinding.inflate(getLayoutInflater());
+ mDiagnosticInfoBinding = mBinding.diagnosticInfo;
+ return mBinding.getRoot();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ dispatchOnPauseToEditors();
+ EditorActivityActions.clear();
+
+ if (mFileTreeFragment != null) {
+ mFileTreeFragment.saveTreeState();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ EditorActivityActions.register(this);
+
+ try {
+ checkForCompilerModule();
+ dispatchOnResumeToEditors();
+
+ if (mFileTreeFragment != null) {
+ mFileTreeFragment.listProjectFiles();
+ }
+ } catch (Throwable th) {
+ LOG.error("Failed to update files list", th);
+ getApp().toast(R.string.msg_failed_list_files, Toaster.Type.ERROR);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void checkForCompilerModule() {
+ if (!Environment.isCompilerModuleInstalled()) {
+ final var pd =
+ ProgressDialog.show(
+ this,
+ getString(R.string.title_compiler_module_install),
+ getString(R.string.msg_compiler_module_install),
+ true,
+ false);
+
+ final CompletableFuture future =
+ CompletableFuture.supplyAsync(
+ () -> {
+ final var tmpModule =
+ new File(Environment.TMP_DIR, "compiler-module.zip");
+ if (!ResourceUtils.copyFileFromAssets(
+ ToolsManager.getCommonAsset("compiler-module.zip"),
+ tmpModule.getAbsolutePath())) {
+ throw new CompletionException(
+ new RuntimeException(
+ "Unable to copy compiler-module.zip"));
+ }
+
+ try {
+ ZipUtils.unzipFile(tmpModule, Environment.COMPILER_MODULE);
+ } catch (Throwable e) {
+ throw new CompletionException(e);
+ }
+
+ if (!Environment.isCompilerModuleInstalled()) {
+ throw new CompletionException(
+ new RuntimeException("Unknown error"));
+ }
+
+ try {
+ FileUtils.delete(tmpModule);
+ } catch (Exception e) {
+ // ignored
+ }
+
+ return true;
+ });
+
+ future.whenComplete(
+ (result, error) -> {
+ pd.dismiss();
+
+ if (error != null) {
+ showCompilerModuleInstallError(error);
+ return;
+ }
+
+ getApp().toast(
+ getString(R.string.msg_compiler_module_installed),
+ Toaster.Type.SUCCESS);
+ });
+ }
+ }
+
+ private void dispatchOnResumeToEditors() {
+ CompletableFuture.runAsync(
+ () -> {
+ for (int i = 0; i < mViewModel.getOpenedFileCount(); i++) {
+ final var editor = getEditorAtIndex(i);
+ if (editor != null) {
+ editor.onResume();
+ }
+ }
+ });
+ }
+
+ private void showCompilerModuleInstallError(Throwable error) {
+ final var stacktrace = ThrowableUtils.getFullStackTrace(error);
+ final var builder = DialogUtils.newMaterialDialogBuilder(this);
+ builder.setTitle(R.string.title_installation_failed);
+ builder.setMessage(getString(R.string.msg_compiler_module_install_failed, stacktrace));
+ builder.setCancelable(false);
+ builder.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss());
+ builder.setNegativeButton(
+ R.string.copy,
+ (dialog, which) -> {
+ ClipboardUtils.copyText(stacktrace);
+ dialog.dismiss();
+ });
+ builder.show();
+ }
+
+ private void dispatchOnPauseToEditors() {
+ CompletableFuture.runAsync(
+ () -> {
+ for (int i = 0; i < mViewModel.getOpenedFileCount(); i++) {
+ final var editor = getEditorAtIndex(i);
+ if (editor != null) {
+ editor.onPause();
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ closeProject(false);
+ try {
+ unregisterReceiver(mLogReceiver);
+ } catch (Throwable th) {
+ LOG.error("Failed to unregister LogReceiver", th);
+ }
+ unbindService(mGradleServiceConnection);
+ super.onDestroy();
+ }
+
+ private void initializeProject() {
+ final var androidProject = getAndroidProject();
+ if (androidProject == null) {
+ LOG.error("Cannot initialize project. Project model is null.");
+ return;
+ }
+
+ final var projectDir = new File(androidProject.getProjectPath());
+ if (!projectDir.exists()) {
+ LOG.error("Project directory does not exist. Cannot initialize project");
+ return;
+ }
+
+ final var future = mBuildService.initializeProject(projectDir);
+ future.whenComplete(
+ (result, error) -> {
+ if (result == null || error != null) {
+ LOG.error(
+ "An error occurred initializing the project with Tooling API",
+ error);
+ return;
+ }
+
+ LOG.debug(
+ "Initialized project:",
+ JSONUtility.toolingGson.toJson(result.getProject()));
+ LOG.debug(
+ "Sync issues:", JSONUtility.toolingGson.toJson(result.getSyncIssues()));
+ });
+ }
+
private void setupDrawerToggle() {
ActionBarDrawerToggle toggle =
new ActionBarDrawerToggle(
@@ -332,655 +1314,6 @@ public class EditorActivity extends StudioActivity
}
}
- @Override
- public void onBackPressed() {
- if (mBinding.getRoot().isDrawerOpen(GravityCompat.END)) {
- mBinding.getRoot().closeDrawer(GravityCompat.END);
- } else if (mBinding.getRoot().isDrawerOpen(GravityCompat.START)) {
- mBinding.getRoot().closeDrawer(GravityCompat.START);
- } else if (getDaemonStatusFragment().isShowing()) {
- getDaemonStatusFragment().dismiss();
- } else if (mFileOptionsFragment != null && mFileOptionsFragment.isShowing()) {
- mFileOptionsFragment.dismiss();
- } else if (mEditorBottomSheet.getState() == BottomSheetBehavior.STATE_EXPANDED) {
- mEditorBottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED);
- } else {
- confirmProjectClose();
- }
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- dispatchOnPauseToEditors();
- EditorActivityActions.clear();
-
- if (mFileTreeFragment != null) {
- mFileTreeFragment.saveTreeState();
- }
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- EditorActivityActions.register(this);
-
- try {
- checkForCompilerModule();
- dispatchOnResumeToEditors();
-
- if (mFileTreeFragment != null) {
- mFileTreeFragment.listProjectFiles();
- }
- } catch (Throwable th) {
- LOG.error("Failed to update files list", th);
- getApp().toast(R.string.msg_failed_list_files, Toaster.Type.ERROR);
- }
- }
-
- @Override
- protected void onDestroy() {
- closeProject(false);
- try {
- unregisterReceiver(mLogReceiver);
- } catch (Throwable th) {
- LOG.error("Failed to unregister LogReceiver", th);
- }
- super.onDestroy();
- }
-
- @SuppressLint("RestrictedApi")
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- if (menu instanceof MenuBuilder) {
- MenuBuilder builder = (MenuBuilder) menu;
- builder.setOptionalIconsVisible(true);
- }
-
- return true;
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- ensureToolbarMenu(menu);
- return true;
- }
-
- public void ensureToolbarMenu(@NonNull Menu menu) {
- menu.clear();
-
- final var data = new ActionData();
- final var currentEditor = getCurrentEditor();
- data.put(Context.class, EditorActivity.this);
- data.put(CodeEditorView.class, currentEditor);
-
- if (currentEditor != null) {
- data.put(IDEEditor.class, currentEditor.getEditor());
- data.put(File.class, currentEditor.getEditor().getFile());
- }
-
- ActionsRegistry.getInstance()
- .fillMenu(
- data,
- com.itsaky.androidide.actions.ActionItem.Location.EDITOR_TOOLBAR,
- menu);
- }
-
- @Override
- public EditorActivity provide() {
- return this;
- }
-
- @Override
- public EditorActivity provideEditorActivity() {
- return this;
- }
-
- @Override
- public AndroidProject provideAndroidProject() {
- return getAndroidProject();
- }
-
- @Override
- public IDEProject provideIDEProject() {
- return getIDEProject();
- }
-
- public void handleSearchResults(Map> results) {
- setSearchResultAdapter(
- new com.itsaky.androidide.adapters.SearchListAdapter(
- results,
- file -> {
- openFile(file);
- hideViewOptions();
- },
- match -> {
- openFileAndSelect(match.file, match);
- hideViewOptions();
- }));
-
- showSearchResults();
-
- if (mSearchingProgress != null && mSearchingProgress.isShowing()) {
- mSearchingProgress.dismiss();
- }
- }
-
- public void appendApkLog(LogLine line) {
- final var logFragment = bottomSheetTabAdapter.getLogFragment();
- if (logFragment != null) {
- logFragment.appendLog(line);
- }
- }
-
- public void appendBuildOut(final String str) {
- final var frag = bottomSheetTabAdapter.getBuildOutputFragment();
-
- if (frag != null) {
- frag.appendOutput(str);
- }
- }
-
- public void showDaemonStatus() {
- ShellServer shell = getApp().newShell(t -> getDaemonStatusFragment().append(t));
- shell.bgAppend(String.format("echo '%s'", getString(R.string.msg_getting_daemom_status)));
- shell.bgAppend(
- String.format(
- "cd '%s' && sh gradlew --status",
- Objects.requireNonNull(getAndroidProject()).getProjectPath()));
- if (!getDaemonStatusFragment().isShowing()) {
- getDaemonStatusFragment().show(getSupportFragmentManager(), "daemon_status");
- }
- }
-
- public void hideViewOptions() {
- if (mEditorBottomSheet.getState() != BottomSheetBehavior.STATE_COLLAPSED) {
- mEditorBottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED);
- }
- }
-
- public void showSearchResults() {
- if (mEditorBottomSheet.getState() != BottomSheetBehavior.STATE_EXPANDED) {
- mEditorBottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED);
- }
-
- final int index =
- bottomSheetTabAdapter.findIndexOfFragmentByClass(SearchResultFragment.class);
- if (index >= 0 && index < mBinding.bottomSheet.tabs.getTabCount()) {
- final var tab = mBinding.bottomSheet.tabs.getTabAt(index);
- if (tab != null) {
- tab.select();
- }
- }
- }
-
- public void handleDiagnosticsResultVisibility(boolean errorVisible) {
- runOnUiThread(
- () -> {
- final var diagnosticsFragment = bottomSheetTabAdapter.getDiagnosticsFragment();
- if (diagnosticsFragment != null) {
- diagnosticsFragment.handleResultVisibility(errorVisible);
- }
- });
- }
-
- public void handleSearchResultVisibility(boolean errorVisible) {
- runOnUiThread(
- () -> {
- final var searchResultFragment =
- bottomSheetTabAdapter.getSearchResultFragment();
- if (searchResultFragment != null) {
- searchResultFragment.handleResultVisibility(errorVisible);
- }
- });
- }
-
- public void setStatus(final CharSequence text) {
- try {
- runOnUiThread(() -> mBinding.bottomSheet.statusText.setText(text));
- } catch (Throwable th) {
- LOG.error("Failed to update status text", th);
- }
- }
-
- public void showFirstBuildNotice() {
- DialogUtils.newMaterialDialogBuilder(this)
- .setPositiveButton(android.R.string.ok, null)
- .setTitle(R.string.title_first_build)
- .setMessage(R.string.msg_first_build)
- .setCancelable(false)
- .create()
- .show();
- }
-
- @Override
- public void onGroupClick(DiagnosticGroup group) {
- if (group != null
- && group.file != null
- && group.file.exists()
- && FileUtils.isUtf8(group.file)) {
- openFile(group.file);
- hideViewOptions();
- }
- }
-
- @Override
- public void onDiagnosticClick(File file, @NonNull DiagnosticItem diagnostic) {
- openFileAndSelect(file, diagnostic.getRange());
- hideViewOptions();
- }
-
- public ActivityEditorBinding getBinding() {
- return mBinding;
- }
-
- public LayoutDiagnosticInfoBinding getDiagnosticBinding() {
- return mDiagnosticInfoBinding;
- }
-
- public void openFileAndSelect(File file, Range selection) {
- openFile(file, selection);
- final var opened = getEditorForFile(file);
- if (opened != null && opened.getEditor() != null) {
- final var editor = opened.getEditor();
- final var range = editor.validateRange(selection);
- editor.post(
- () -> {
- if (LSPUtils.isEqual(range.getStart(), range.getEnd())) {
- editor.setSelection(
- range.getStart().getLine(), range.getEnd().getColumn());
- } else {
- editor.setSelection(range);
- }
- });
- }
- }
-
- @Override
- public void onTabSelected(@NonNull TabLayout.Tab tab) {
- final var position = tab.getPosition();
- mBinding.editorContainer.setDisplayedChild(position);
-
- final var editorView = getEditorAtIndex(position);
- Objects.requireNonNull(editorView);
-
- editorView.onEditorSelected();
- mViewModel.setCurrentFile(position, editorView.getFile());
- refreshSymbolInput(editorView);
- }
-
- @Override
- public void onTabUnselected(@NonNull TabLayout.Tab tab) {
- final var position = tab.getPosition();
- final var editorView = getEditorAtIndex(position);
- if (editorView != null) {
- final var editor = editorView.getEditor();
- if (editor != null) {
- editor.ensureWindowsDismissed();
- }
- }
- }
-
- @Override
- public void onTabReselected(@NonNull TabLayout.Tab tab) {
- mTabCloseAction.show(tab.view);
- }
-
- @Override
- public boolean onNavigationItemSelected(@NonNull MenuItem p1) {
- final int id = p1.getItemId();
- if (id == R.id.editornav_discuss) {
- getApp().openTelegramGroup();
- } else if (id == R.id.editornav_suggest) {
- getApp().openGitHub();
- } else if (id == R.id.editornav_needHelp) {
- showNeedHelpDialog();
- } else if (id == R.id.editornav_settings) {
- startActivity(new Intent(this, PreferencesActivity.class));
- } else if (id == R.id.editornav_share) {
- startActivity(IntentUtils.getShareTextIntent(getString(R.string.msg_share_app)));
- } else if (id == R.id.editornav_close_project) {
- confirmProjectClose();
- } else if (id == R.id.editornav_terminal) {
- openTerminal();
- }
- mBinding.getRoot().closeDrawer(GravityCompat.START);
- return false;
- }
-
- public void loadFragment(Fragment fragment) {
- super.loadFragment(fragment, mBinding.editorFrameLayout.getId());
- }
-
- public FileTreeFragment getFileTreeFragment() {
- return mFileTreeFragment;
- }
-
- public TreeNode getLastHoldTreeNode() {
- return mLastHeld;
- }
-
- public OptionsListFragment getFileOptionsFragment(File file) {
- mFileOptionsFragment = new OptionsListFragment();
- mFileOptionsFragment.addOption(
- new SheetOption(0, R.drawable.ic_file_copy_path, R.string.copy_path, file));
- mFileOptionsFragment.addOption(
- new SheetOption(1, R.drawable.ic_file_rename, R.string.rename_file, file));
- mFileOptionsFragment.addOption(
- new SheetOption(2, R.drawable.ic_delete, R.string.delete_file, file));
- if (file.isDirectory()) {
- mFileOptionsFragment.addOption(
- new SheetOption(3, R.drawable.ic_new_file, R.string.new_file, file));
- mFileOptionsFragment.addOption(
- new SheetOption(4, R.drawable.ic_new_folder, R.string.new_folder, file));
- }
-
- return mFileOptionsFragment;
- }
-
- public ProgressSheet getProgressSheet(int msg) {
- if (mSearchingProgress != null && mSearchingProgress.isShowing()) {
- mSearchingProgress.dismiss();
- }
- mSearchingProgress = new ProgressSheet();
- mSearchingProgress.setCancelable(false);
- mSearchingProgress.setWelcomeTextEnabled(false);
- mSearchingProgress.setMessage(getString(msg));
- mSearchingProgress.setSubMessageEnabled(false);
- return mSearchingProgress;
- }
-
- public MaterialBanner getSyncBanner() {
- return mBinding.syncBanner
- .setContentTextColor(ContextCompat.getColor(this, R.color.primaryTextColor))
- .setBannerBackgroundColor(ContextCompat.getColor(this, R.color.primaryLightColor))
- .setButtonTextColor(ContextCompat.getColor(this, R.color.secondaryColor))
- .setIcon(R.drawable.ic_sync)
- .setContentText(R.string.msg_sync_needed);
- }
-
- public TextSheetFragment getDaemonStatusFragment() {
- return mDaemonStatusFragment == null
- ? mDaemonStatusFragment =
- new TextSheetFragment()
- .setTextSelectable(true)
- .setTitleText(R.string.gradle_daemon_status)
- : mDaemonStatusFragment;
- }
-
- public void setDiagnosticsAdapter(@NonNull final DiagnosticsAdapter adapter) {
- runOnUiThread(
- () -> {
- final var diagnosticsFragment = bottomSheetTabAdapter.getDiagnosticsFragment();
- if (diagnosticsFragment != null) {
- diagnosticsFragment.setAdapter(adapter);
- }
- });
- }
-
- public void setSearchResultAdapter(@NonNull final SearchListAdapter adapter) {
- runOnUiThread(
- () -> {
- final var searchResultFragment =
- bottomSheetTabAdapter.getSearchResultFragment();
- if (searchResultFragment != null) {
- searchResultFragment.setAdapter(adapter);
- }
- });
- }
-
- @Override
- public void showFileOptions(File thisFile, TreeNode node) {
- mLastHeld = node;
- getFileOptionsFragment(thisFile)
- .show(getSupportFragmentManager(), TAG_FILE_OPTIONS_FRAGMENT);
- }
-
- @SuppressWarnings("UnusedReturnValue")
- public boolean saveAll() {
- return saveAll(true);
- }
-
- public boolean saveAll(boolean notify) {
- return saveAll(notify, false);
- }
-
- public boolean saveAll(boolean notify, boolean canProcessResources) {
- SaveResult result = saveAllResult();
-
- if (notify) {
- getApp().toast(R.string.all_saved, Toaster.Type.SUCCESS);
- }
-
- if (result.gradleSaved) {
- notifySyncNeeded();
- }
-
- if (result.xmlSaved && canProcessResources && getBuildService() != null) {
- getBuildService().updateResourceClasses();
- }
-
- return result.gradleSaved;
- }
-
- public SaveResult saveAllResult() {
- SaveResult result = new SaveResult();
- for (int i = 0; i < mViewModel.getOpenedFileCount(); i++) {
- saveResult(i, result);
- }
-
- return result;
- }
-
- public void saveResult(int index, SaveResult result) {
- if (index >= 0 && index < mViewModel.getOpenedFileCount()) {
- var frag = getEditorAtIndex(index);
- if (frag == null || frag.getFile() == null) {
- return;
- }
-
- // Must be called before frag.save()
- // Otherwise, it'll always return false
- final boolean modified = frag.isModified();
-
- frag.save();
-
- final boolean isGradle = frag.getFile().getName().endsWith(".gradle");
- final boolean isXml = frag.getFile().getName().endsWith(".xml");
-
- if (!result.gradleSaved) {
- result.gradleSaved = modified && isGradle;
- }
-
- if (!result.xmlSaved) {
- result.xmlSaved = modified && isXml;
- }
- }
-
- var modified = false;
- for (var file : mViewModel.getOpenedFiles()) {
- var editor = getEditorForFile(file);
- if (editor == null) {
- continue;
- }
- modified = modified || editor.isModified();
- }
-
- final boolean finalModified = modified;
- ThreadUtils.runOnUiThread(() -> mViewModel.setFilesModified(finalModified));
- }
-
- public void install(@NonNull File apk) {
- LOG.debug("Installing APK:", apk);
- if (apk.exists()) {
- Intent i = IntentUtils.getInstallAppIntent(apk);
- if (i != null) {
- startActivity(i);
- } else {
- getApp().toast(R.string.msg_apk_install_intent_failed, Toaster.Type.ERROR);
- }
- } else {
- LOG.error("APK file does not exist!");
- }
- }
-
- public void updateServices() {
- new TaskExecutor()
- .executeAsync(
- () -> {
- List cps =
- Objects.requireNonNull(getAndroidProject()).getClassPaths();
- getApp().getJavaLanguageServer()
- .configurationChanged(
- new JavaServerConfiguration(
- cps.stream()
- .map(Paths::get)
- .collect(Collectors.toSet())));
- return null;
- },
- __ -> setStatus(getString(R.string.msg_service_started)));
- }
-
- @Override
- public CodeEditorView openFile(File file) {
- return openFile(file, null);
- }
-
- public CodeEditorView openFile(File file, com.itsaky.lsp.models.Range selection) {
- if (selection == null) {
- selection = com.itsaky.lsp.models.Range.NONE;
- }
-
- int index = openFileAndGetIndex(file, selection);
- final var tab = mBinding.tabs.getTabAt(index);
- if (tab != null && index >= 0 && !tab.isSelected()) {
- tab.select();
- }
-
- if (mBinding.editorDrawerLayout.isDrawerOpen(GravityCompat.END)) {
- mBinding.editorDrawerLayout.closeDrawer(GravityCompat.END);
- }
-
- mBinding.editorContainer.setDisplayedChild(index);
-
- try {
- return getEditorAtIndex(index);
- } catch (Throwable th) {
- LOG.error("Unable to get editor fragment at opened file index", index);
- LOG.error(th);
- return null;
- }
- }
-
- @SuppressLint("NotifyDataSetChanged")
- public int openFileAndGetIndex(File file, com.itsaky.lsp.models.Range selection) {
- final var openedFileIndex = findIndexOfEditorByFile(file);
-
- if (openedFileIndex != -1) {
- LOG.error("File is already opened. File: " + file);
- return openedFileIndex;
- }
-
- final var position = mViewModel.getOpenedFileCount();
-
- LOG.info("Opening file at index:", position, "file: ", file);
-
- final var editor = new CodeEditorView(this, file, selection);
- editor.getEditor().subscribeEvent(ContentChangeEvent.class, this::onEditorContentChanged);
- editor.setLayoutParams(
- new ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
-
- mBinding.editorContainer.addView(editor);
- mBinding.editorContainer.setDisplayedChild(position);
- mBinding.tabs.addTab(mBinding.tabs.newTab().setText(file.getName()));
-
- mViewModel.addFile(file);
- mViewModel.setCurrentFile(position, file);
-
- return position;
- }
-
- public void closeFile(int index) {
- if (index >= 0 && index < mViewModel.getOpenedFileCount()) {
- var opened = mViewModel.getOpenedFile(index);
- LOG.info("Closing file:", opened);
- final var editor = getEditorAtIndex(index);
-
- if (editor != null && editor.isModified()) {
- notifyFilesUnsaved(Collections.singletonList(editor), () -> closeFile(index));
- return;
- }
-
- if (editor != null && editor.getEditor() != null) {
- editor.getEditor().close();
- } else {
- LOG.error("Cannot save file before close. Editor instance is null");
- }
-
- mViewModel.removeFile(index);
- mBinding.tabs.removeTabAt(index);
- mBinding.editorContainer.removeViewAt(index);
- } else {
- LOG.error("Invalid file index. Cannot close.");
- return;
- }
-
- mBinding.tabs.requestLayout();
- }
-
- public void closeAll() {
- closeAll(null);
- }
-
- /**
- * Close all opened files. If all the files are saved successfully, then the provided {@link
- * Runnable} will be called.
- *
- * @param onSaved The {@link Runnable} to run when all the files are saved.
- */
- public void closeAll(Runnable onSaved) {
- final var count = mViewModel.getOpenedFileCount();
- final var unsavedFiles =
- mViewModel.getOpenedFiles().stream()
- .map(this::getEditorForFile)
- .filter(editor -> editor != null && editor.isModified())
- .collect(Collectors.toList());
-
- if (unsavedFiles.isEmpty()) {
- // Files were already saved, close all files one by one
- for (int i = 0; i < count; i++) {
- final var editor = getEditorAtIndex(i);
- if (editor != null && editor.getEditor() != null) {
- editor.getEditor().close();
- } else {
- LOG.error("Unable to close file at index:", i);
- }
- }
-
- mViewModel.removeAllFiles();
- mBinding.tabs.removeAllTabs();
- mBinding.tabs.requestLayout();
- mBinding.editorContainer.removeAllViews();
-
- if (onSaved != null) {
- onSaved.run();
- }
- } else {
- // There are unsaved files
- notifyFilesUnsaved(unsavedFiles, () -> closeAll(onSaved));
- }
- }
-
- public boolean areFilesModified() {
- return mViewModel.areFilesModified();
- }
-
- public EditorViewModel getViewModel() {
- return mViewModel;
- }
-
private void notifyFilesUnsaved(List unsavedEditors, Runnable invokeAfter) {
//noinspection ConstantConditions
final var mapped =
@@ -1013,208 +1346,6 @@ public class EditorActivity extends StudioActivity
builder.show();
}
- public void closeOthers() {
- final var unsavedFiles =
- mViewModel.getOpenedFiles().stream()
- .map(this::getEditorForFile)
- .filter(editor -> editor != null && editor.isModified())
- .collect(Collectors.toList());
-
- if (unsavedFiles.isEmpty()) {
- final var file = mViewModel.getCurrentFile();
- for (int i = 0; i < mViewModel.getOpenedFileCount(); i++) {
- final var editor = getEditorAtIndex(i);
-
- // Index of files changes as we keep close files
- // So we compare the files instead of index
- if (editor != null) {
- if (!file.equals(editor.getFile())) {
- closeFile(i);
- }
- } else {
- LOG.error("Unable to save file at index:", i);
- }
- }
- } else {
- notifyFilesUnsaved(unsavedFiles, this::closeOthers);
- }
- }
-
- @Nullable
- public CodeEditorView getCurrentEditor() {
- if (mViewModel.getCurrentFileIndex() != -1) {
- return getEditorAtIndex(mViewModel.getCurrentFileIndex());
- }
-
- return null;
- }
-
- @Nullable
- public CodeEditorView getEditorAtIndex(final int index) {
- return (CodeEditorView) mBinding.editorContainer.getChildAt(index);
- }
-
- @Nullable
- public CodeEditorView getEditorForFile(@NonNull final File file) {
- for (int i = 0; i < mViewModel.getOpenedFileCount(); i++) {
- final CodeEditorView editor = (CodeEditorView) mBinding.editorContainer.getChildAt(i);
- if (file.equals(editor.getFile())) {
- return editor;
- }
- }
-
- return null;
- }
-
- public int findIndexOfEditorByFile(File file) {
- if (file == null) {
- LOG.error("Cannot find index of a null file.");
- return -1;
- }
-
- for (int i = 0; i < mViewModel.getOpenedFileCount(); i++) {
- final var opened = mViewModel.getOpenedFile(i);
- if (opened.equals(file)) {
- return i;
- }
- }
-
- return -1;
- }
-
- public void previewLayout() {
- try {
-
- if (getCurrentEditor() == null || getCurrentEditor().getFile() == null) {
- LOG.error("No file is opened. Cannot preview layout.");
- return;
- }
-
- saveAll(false);
-
- final Intent intent = new Intent(this, DesignerActivity.class);
- intent.putExtra(
- DesignerActivity.KEY_LAYOUT_PATH,
- getCurrentEditor().getFile().getAbsolutePath());
- intent.putStringArrayListExtra(DesignerActivity.KEY_RES_DIRS, getResourceDirectories());
- LOG.info("Launching UI Designer...");
- mUIDesignerLauncher.launch(intent);
- } catch (Throwable th) {
- LOG.error(getString(R.string.err_cannot_preview_layout), th);
- getApp().toast(R.string.msg_cannot_preview_layout, Toaster.Type.ERROR);
- }
- }
-
- /////////////////////////////////////////////////
- ////////////// PRIVATE APIS /////////////////////
- /////////////////////////////////////////////////
-
- private void onEditorContentChanged(ContentChangeEvent event, Unsubscribe unsubscribe) {
- if (event.getAction() != ContentChangeEvent.ACTION_SET_NEW_TEXT) {
- mViewModel.setFilesModified(true);
- }
- }
-
- @SuppressWarnings("deprecation")
- private void checkForCompilerModule() {
- if (!Environment.isCompilerModuleInstalled()) {
- final var pd =
- ProgressDialog.show(
- this,
- getString(R.string.title_compiler_module_install),
- getString(R.string.msg_compiler_module_install),
- true,
- false);
-
- final CompletableFuture future =
- CompletableFuture.supplyAsync(
- () -> {
- final var tmpModule =
- new File(Environment.TMP_DIR, "compiler-module.zip");
- if (!ResourceUtils.copyFileFromAssets(
- ToolsManager.getCommonAsset("compiler-module.zip"),
- tmpModule.getAbsolutePath())) {
- throw new CompletionException(
- new RuntimeException(
- "Unable to copy compiler-module.zip"));
- }
-
- try {
- ZipUtils.unzipFile(tmpModule, Environment.COMPILER_MODULE);
- } catch (Throwable e) {
- throw new CompletionException(e);
- }
-
- if (!Environment.isCompilerModuleInstalled()) {
- throw new CompletionException(
- new RuntimeException("Unknown error"));
- }
-
- try {
- FileUtils.delete(tmpModule);
- } catch (Exception e) {
- // ignored
- }
-
- return true;
- });
-
- future.whenComplete(
- (result, error) -> {
- pd.dismiss();
-
- if (error != null) {
- showCompilerModuleInstallError(error);
- return;
- }
-
- getApp().toast(
- getString(R.string.msg_compiler_module_installed),
- Toaster.Type.SUCCESS);
- });
- }
- }
-
- private void showCompilerModuleInstallError(Throwable error) {
- final var stacktrace = ThrowableUtils.getFullStackTrace(error);
- final var builder = DialogUtils.newMaterialDialogBuilder(this);
- builder.setTitle(R.string.title_installation_failed);
- builder.setMessage(getString(R.string.msg_compiler_module_install_failed, stacktrace));
- builder.setCancelable(false);
- builder.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss());
- builder.setNegativeButton(
- R.string.copy,
- (dialog, which) -> {
- ClipboardUtils.copyText(stacktrace);
- dialog.dismiss();
- });
- builder.show();
- }
-
- private void dispatchOnPauseToEditors() {
- CompletableFuture.runAsync(
- () -> {
- for (int i = 0; i < mViewModel.getOpenedFileCount(); i++) {
- final var editor = getEditorAtIndex(i);
- if (editor != null) {
- editor.onPause();
- }
- }
- });
- }
-
- private void dispatchOnResumeToEditors() {
- CompletableFuture.runAsync(
- () -> {
- for (int i = 0; i < mViewModel.getOpenedFileCount(); i++) {
- final var editor = getEditorAtIndex(i);
- if (editor != null) {
- editor.onResume();
- }
- }
- });
- }
-
@Contract(pure = true)
private void onGetUIDesignerResult(@NonNull ActivityResult result) {
final var index = mBinding.editorContainer.getDisplayedChild();
@@ -1237,24 +1368,6 @@ public class EditorActivity extends StudioActivity
}
}
- @NonNull
- private ArrayList getResourceDirectories() {
- final var dirs = new ArrayList();
- 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());
- }
- }
- }
- }
- return dirs;
- }
-
private void openTerminal() {
final Intent intent = new Intent(this, TerminalActivity.class);
intent.putExtra(
@@ -1290,30 +1403,6 @@ public class EditorActivity extends StudioActivity
.toArray(File[]::new)));
}
- @Nullable
- private AndroidProject getAndroidProject() {
- if (mViewModel == null) {
- return null;
- }
-
- return mViewModel.getAndroidProject();
- }
-
- @Nullable
- private IDEProject getIDEProject() {
- if (mViewModel == null) {
- return null;
- }
-
- return mViewModel.getIDEProject();
- }
-
- public void setIDEProject(IDEProject project) {
- if (mViewModel != null) {
- mViewModel.setIDEProject(project);
- }
- }
-
private void setupDiagnosticInfo() {
GradientDrawable gd = new GradientDrawable();
gd.setShape(GradientDrawable.RECTANGLE);
@@ -1331,6 +1420,15 @@ public class EditorActivity extends StudioActivity
private void startServices() {
+ if (bindService(
+ new Intent(this, GradleBuildService.class),
+ mGradleServiceConnection,
+ Context.BIND_AUTO_CREATE)) {
+ LOG.info("Bind request for Gradle build service was successful...");
+ } else {
+ LOG.error("Gradle build service doesn't exist or the IDE is not allowed to access it.");
+ }
+
if (!IDELanguageClientImpl.isInitialized()) {
IDELanguageClientImpl.initialize(this);
}
@@ -1342,7 +1440,7 @@ public class EditorActivity extends StudioActivity
// But still we do...
mFileOptionsHandler.start();
mBuildServiceHandler.start();
- getBuildServiceHandler().assembleDebug(false);
+// getBuildServiceHandler().assembleDebug(false);
}
private void initializeLanguageServers() {
@@ -1402,21 +1500,6 @@ public class EditorActivity extends StudioActivity
}
}
- private void refreshSymbolInput(@NonNull CodeEditorView frag) {
- symbolInput.bindEditor(frag.getEditor());
- symbolInput.setSymbols(Symbols.forFile(frag.getFile()));
- }
-
- private void notifySyncNeeded() {
- if (getBuildService() != null && !getBuildService().isBuilding()) {
- getSyncBanner()
- .setNegative(android.R.string.cancel, null)
- .setPositive(
- android.R.string.ok, v -> getBuildServiceHandler().assembleDebug(false))
- .show();
- }
- }
-
private void closeProject(boolean manualFinish) {
stopServices();
@@ -1447,10 +1530,6 @@ public class EditorActivity extends StudioActivity
builder.show();
}
- public AlertDialog getFindInProjectDialog() {
- return mFindInProjectDialog == null ? createFindInProjectDialog() : mFindInProjectDialog;
- }
-
@Nullable
private AlertDialog createFindInProjectDialog() {
if (getAndroidProject() == null
@@ -1547,14 +1626,6 @@ public class EditorActivity extends StudioActivity
registerReceiver(mLogReceiver, filter);
}
- private IDEService getBuildService() {
- return mBuildServiceHandler.getService();
- }
-
- public BuildServiceHandler getBuildServiceHandler() {
- return mBuildServiceHandler;
- }
-
private void showNeedHelpDialog() {
MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(R.string.need_help);
@@ -1605,11 +1676,4 @@ public class EditorActivity extends StudioActivity
}
});
}
-
- @Override
- public void onOptionsClick(SheetOption option) {
- if (mFileOptionsHandler != null) {
- mFileOptionsHandler.onOptionsClick(option);
- }
- }
}
diff --git a/app/src/main/java/com/itsaky/androidide/services/GradleBuildService.java b/app/src/main/java/com/itsaky/androidide/services/GradleBuildService.java
new file mode 100644
index 000000000..ae2e95516
--- /dev/null
+++ b/app/src/main/java/com/itsaky/androidide/services/GradleBuildService.java
@@ -0,0 +1,233 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.services;
+
+import static com.itsaky.androidide.utils.ILogger.newInstance;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.itsaky.androidide.EditorActivity;
+import com.itsaky.androidide.R;
+import com.itsaky.androidide.app.BaseApplication;
+import com.itsaky.androidide.builder.BuildService;
+import com.itsaky.androidide.shell.CommonProcessExecutor;
+import com.itsaky.androidide.shell.ProcessStreamsHolder;
+import com.itsaky.androidide.tooling.api.IToolingApiClient;
+import com.itsaky.androidide.tooling.api.IToolingApiServer;
+import com.itsaky.androidide.tooling.api.messages.InitializeProjectMessage;
+import com.itsaky.androidide.tooling.api.messages.result.InitializeResult;
+import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult;
+import com.itsaky.androidide.tooling.api.util.ToolingApiLauncher;
+import com.itsaky.androidide.utils.Environment;
+import com.itsaky.androidide.utils.ILogger;
+import com.itsaky.androidide.utils.InputStreamLineReader;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * A foreground service that handles interaction with the Gradle Tooling API.
+ *
+ * @author Akash Yadav
+ */
+public class GradleBuildService extends Service implements BuildService, IToolingApiClient {
+
+ private static final ILogger LOG = newInstance("GradleBuildService");
+ private static final int NOTIFICATION_ID = R.string.app_name;
+ private final ILogger SERVER_LOGGER = newInstance("ToolingApiServer");
+ private final IBinder mBinder = new GradleServiceBinder();
+ private boolean isToolingServerStarted = false;
+ private Thread toolingServerThread;
+ private NotificationManager notificationManager;
+ private IToolingApiServer server;
+
+ @Override
+ public void onCreate() {
+ notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ showNotification();
+ }
+
+ private void showNotification() {
+ final var ticker = getString(R.string.title_gradle_service_notification_ticker);
+ final var title = getString(R.string.title_gradle_service_notification);
+ final var message = getString(R.string.msg_gradle_service_notification);
+ final var intent =
+ PendingIntent.getActivity(this, 0, new Intent(this, EditorActivity.class), 0);
+ final var notification =
+ new Notification.Builder(this, BaseApplication.NOTIFICATION_GRADLE_BUILD_SERVICE)
+ .setSmallIcon(R.mipmap.ic_launcher_round)
+ .setTicker(ticker)
+ .setWhen(System.currentTimeMillis())
+ .setContentTitle(title)
+ .setContentText(message)
+ .setContentIntent(intent)
+ .build();
+
+ LOG.info("Showing notification to user...");
+ startForeground(NOTIFICATION_ID, notification);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ // No point in restarting the service if it really gets killed.
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ LOG.info("Service is being destroyed.", "Dismissing the shown notification...");
+ notificationManager.cancel(NOTIFICATION_ID);
+ if (toolingServerThread != null) {
+ toolingServerThread.interrupt();
+ }
+
+ isToolingServerStarted = false;
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @NonNull
+ @Override
+ public CompletableFuture initializeProject(@NonNull File rootDir) {
+ checkServerStarted();
+ return server.initialize(new InitializeProjectMessage(rootDir));
+ }
+
+ @NonNull
+ @Override
+ public CompletableFuture executeTasks(@NonNull String... tasks) {
+ checkServerStarted();
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public CompletableFuture executeProjectTasks(
+ @NonNull String projectPath, @NonNull String... tasks) {
+ checkServerStarted();
+ return null;
+ }
+
+ private void checkServerStarted() {
+ if (!isToolingServerStarted) {
+ throw new IllegalStateException("Tooling API server has not been started");
+ }
+ }
+
+ public void startToolingServer(@Nullable OnServerStartListener listener) {
+ if (toolingServerThread != null && toolingServerThread.isAlive()) {
+ throw new IllegalStateException("Tooling API server is already started!");
+ }
+
+ toolingServerThread = new Thread(new ToolingServerRunner(listener));
+ toolingServerThread.start();
+ }
+
+ protected void onServerStarted() {}
+
+ protected void onServerExited(int exitCode) {
+ LOG.warn("Tooling API process terminated with exit code:", exitCode);
+ stopForeground(true);
+ }
+
+ private void startServerOutputReader(InputStream err) {
+ new Thread(new InputStreamLineReader(err, this::onServerOutput)).start();
+ }
+
+ protected void onServerOutput(String line) {
+ SERVER_LOGGER.warn(line);
+ }
+
+ public class GradleServiceBinder extends Binder {
+ public GradleBuildService getService() {
+ return GradleBuildService.this;
+ }
+ }
+
+ private class ToolingServerRunner implements Runnable {
+
+ @Nullable private final OnServerStartListener listener;
+
+ public ToolingServerRunner(@Nullable OnServerStartListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void run() {
+ try {
+ LOG.info("Starting tooling API server...");
+ final var serverStreams = new ProcessStreamsHolder();
+ final var executor = new CommonProcessExecutor();
+ executor.execAsync(
+ serverStreams,
+ GradleBuildService.this::onServerExited,
+ false,
+ Environment.JAVA.getAbsolutePath(),
+ "-jar",
+ Environment.TOOLING_API_JAR.getAbsolutePath());
+
+ final var launcher =
+ ToolingApiLauncher.createClientLauncher(
+ GradleBuildService.this, serverStreams.in, serverStreams.out);
+ final var future = launcher.startListening();
+
+ GradleBuildService.this.startServerOutputReader(serverStreams.err);
+ GradleBuildService.this.server = launcher.getRemoteProxy();
+ GradleBuildService.this.isToolingServerStarted = true;
+
+ if (listener != null) {
+ listener.onServerStarted();
+ }
+
+ // Wait(block) until the process terminates
+ try {
+ future.get();
+ } catch (ExecutionException | InterruptedException e) {
+ LOG.error(
+ "An error occurred while waiting for tooling API server to terminate");
+ }
+
+ } catch (Throwable e) {
+ LOG.error("Unable to start tooling API server", e);
+ }
+ }
+ }
+
+ /** Callback to listen for Tooling API server start event. */
+ public interface OnServerStartListener {
+
+ /** Called when the tooling API server has been successfully started. */
+ void onServerStarted();
+ }
+}
diff --git a/app/src/main/java/com/itsaky/androidide/utils/JSONUtility.java b/app/src/main/java/com/itsaky/androidide/utils/JSONUtility.java
index 80b4f7bec..316b267ec 100755
--- a/app/src/main/java/com/itsaky/androidide/utils/JSONUtility.java
+++ b/app/src/main/java/com/itsaky/androidide/utils/JSONUtility.java
@@ -1,8 +1,6 @@
-/************************************************************************************
+/*
* 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
@@ -16,14 +14,21 @@
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see .
*
- **************************************************************************************/
-
+ */
package com.itsaky.androidide.utils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import com.itsaky.androidide.tooling.api.util.ToolingApiLauncher;
public class JSONUtility {
public static final Gson gson = new Gson();
+ public static final Gson toolingGson;
public static final Gson prettyPrinter = new GsonBuilder().setPrettyPrinting().create();
+
+ static {
+ final var builder = new GsonBuilder();
+ ToolingApiLauncher.configureGson(builder);
+ toolingGson = builder.create();
+ }
}
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100755
index 61da551c5..000000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100755
index db5080a75..000000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100755
index da31a871c..000000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100755
index b216f2d31..000000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100755
index e96783ccc..000000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b36404f8c..e40abf41d 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -357,6 +357,10 @@
Case sensitive
Regex
Replacing...
+
+ Gradle build service started
+ Gradle build service
+ AndroidIDE\'s Gradle build service is running in the background.
diff --git a/build-tools/builder-api/src/main/java/com/itsaky/androidide/builder/BuildService.kt b/build-tools/builder-api/src/main/java/com/itsaky/androidide/builder/BuildService.kt
index bb592eb82..72a8afecb 100644
--- a/build-tools/builder-api/src/main/java/com/itsaky/androidide/builder/BuildService.kt
+++ b/build-tools/builder-api/src/main/java/com/itsaky/androidide/builder/BuildService.kt
@@ -29,7 +29,7 @@ import java.util.concurrent.*
* @author Akash Yadav
*/
interface BuildService {
-
+
/**
* Initialize the project.
*
diff --git a/build-tools/tooling-api-impl/build.gradle b/build-tools/tooling-api-impl/build.gradle
index aedbd42b8..19e419e5e 100644
--- a/build-tools/tooling-api-impl/build.gradle
+++ b/build-tools/tooling-api-impl/build.gradle
@@ -60,7 +60,6 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation 'org.gradle:gradle-tooling-api:7.4.2'
- implementation "org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.12.0"
testImplementation tests.junit
testImplementation tests.google_truth
diff --git a/build-tools/tooling-api-impl/src/test/test-home/.androidide/init/model.jar b/build-tools/tooling-api-impl/src/test/test-home/.androidide/init/model.jar
index 532157257..ec7cf51f7 100644
Binary files a/build-tools/tooling-api-impl/src/test/test-home/.androidide/init/model.jar and b/build-tools/tooling-api-impl/src/test/test-home/.androidide/init/model.jar differ
diff --git a/build-tools/tooling-api/build.gradle b/build-tools/tooling-api/build.gradle
index f7bd31b6f..b44fbbcdc 100644
--- a/build-tools/tooling-api/build.gradle
+++ b/build-tools/tooling-api/build.gradle
@@ -27,14 +27,14 @@ java {
dependencies {
implementation project (path: ':build-tools:logger')
- implementation project (path: ':build-tools:tooling-api-model')
+ api project (path: ':build-tools:tooling-api-model')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
- implementation "org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.12.0"
testImplementation tests.junit
testImplementation tests.google_truth
+ api "org.eclipse.lsp4j:org.eclipse.lsp4j.jsonrpc:0.12.0"
api google.gson
}
diff --git a/common/src/main/java/com/itsaky/androidide/app/BaseApplication.java b/common/src/main/java/com/itsaky/androidide/app/BaseApplication.java
index eac89c1f9..05ebc9849 100755
--- a/common/src/main/java/com/itsaky/androidide/app/BaseApplication.java
+++ b/common/src/main/java/com/itsaky/androidide/app/BaseApplication.java
@@ -49,8 +49,7 @@ import java.util.Arrays;
public abstract class BaseApplication extends MultiDexApplication {
- public static final String NOTIFICATION_ID_UPDATE = "17571";
- public static final String NOTIFICATION_ID_DEVS = "17572";
+ public static final String NOTIFICATION_GRADLE_BUILD_SERVICE = "17571";
public static final String TELEGRAM_GROUP_URL = "https://t.me/androidide_discussions";
public static final String GITHUB_URL = "https://github.com/itsaky/AndroidIDE";
public static final String WEBSITE = "https://androidide.com";
@@ -103,44 +102,18 @@ public abstract class BaseApplication extends MultiDexApplication {
createNotificationChannels();
}
-
- @SuppressLint("NewApi")
+
private void createNotificationChannels() {
NotificationChannel updateChannel =
new NotificationChannel(
- NOTIFICATION_ID_UPDATE,
- getNotificationChannelNameForId(NOTIFICATION_ID_UPDATE),
+ NOTIFICATION_GRADLE_BUILD_SERVICE,
+ getString(R.string.title_gradle_service_notification_channel),
NotificationManager.IMPORTANCE_HIGH);
updateChannel.enableLights(true);
updateChannel.enableVibration(true);
updateChannel.setLightColor(Color.RED);
updateChannel.setVibrationPattern(new long[] {10, 50, 10});
NotificationManagerCompat.from(this).createNotificationChannel(updateChannel);
-
- NotificationChannel devsChannels =
- new NotificationChannel(
- NOTIFICATION_ID_DEVS,
- getNotificationChannelNameForId(NOTIFICATION_ID_DEVS),
- NotificationManager.IMPORTANCE_MIN);
- devsChannels.enableLights(true);
- devsChannels.enableVibration(true);
- devsChannels.setLightColor(Color.BLUE);
- devsChannels.setVibrationPattern(new long[] {10, 50, 10});
- NotificationManagerCompat.from(this).createNotificationChannel(devsChannels);
- }
-
- public String getNotificationChannelNameForId(@NonNull String id) {
- if (id.equals(NOTIFICATION_ID_UPDATE)) return getUpdateNotificationChannelName();
- else if (id.equals(NOTIFICATION_ID_DEVS)) return getDevNotificationChannelName();
- else return "AndroidIDE Notifications";
- }
-
- private String getUpdateNotificationChannelName() {
- return getString(R.string.cms_channel_id_update);
- }
-
- private String getDevNotificationChannelName() {
- return getString(R.string.cms_channel_id_devs);
}
public ShellServer newShell(ShellServer.Callback callback) {
@@ -159,12 +132,6 @@ public abstract class BaseApplication extends MultiDexApplication {
return shellServer;
}
- public void writeException(Throwable th) {
- FileUtil.writeFile(
- new File(FileUtil.getExternalStorageDir(), "idelog.txt").getAbsolutePath(),
- ThrowableUtils.getFullStackTrace(th));
- }
-
public File getRootDir() {
return new File(getIDEDataDir(), "framework");
}
@@ -174,6 +141,12 @@ public abstract class BaseApplication extends MultiDexApplication {
return Environment.mkdirIfNotExits(new File("/data/data/com.itsaky.androidide/files"));
}
+ public void writeException(Throwable th) {
+ FileUtil.writeFile(
+ new File(FileUtil.getExternalStorageDir(), "idelog.txt").getAbsolutePath(),
+ ThrowableUtils.getFullStackTrace(th));
+ }
+
@NonNull
@Contract(" -> new")
public final File getLogSenderDir() {
@@ -192,26 +165,22 @@ public abstract class BaseApplication extends MultiDexApplication {
return mPrefsManager;
}
- public File getProjectsDir() {
- return Environment.PROJECTS_DIR;
- }
-
public File[] listProjects() {
return getProjectsDir().listFiles(File::isDirectory);
}
- public StudioUtils getUtils() {
- return mUtils == null ? mUtils = new StudioUtils(this) : mUtils;
- }
-
- public void toast(String msg, Toaster.Type type) {
- getUtils().toast(msg, type);
+ public File getProjectsDir() {
+ return Environment.PROJECTS_DIR;
}
public void toast(int msg, Toaster.Type type) {
getUtils().toast(msg, type);
}
+ public StudioUtils getUtils() {
+ return mUtils == null ? mUtils = new StudioUtils(this) : mUtils;
+ }
+
public void toastLong(String msg, Toaster.Type type) {
getUtils().toastLong(msg, type);
}
@@ -241,6 +210,10 @@ public abstract class BaseApplication extends MultiDexApplication {
}
}
+ public void toast(String msg, Toaster.Type type) {
+ getUtils().toast(msg, type);
+ }
+
public void openGitHub() {
try {
Intent open = new Intent();
diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml
index aae3459d3..9252c2c50 100755
--- a/common/src/main/res/values/strings.xml
+++ b/common/src/main/res/values/strings.xml
@@ -1,6 +1,5 @@
- Notifications from Developers
- App Update Notifications
+ AndroidIDE Gradle build service
Hex color code
Pick
Setting executable permissions…