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 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…