From 29097cdc66d96cc07690ac7769e1e06a6d21dfe2 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Fri, 29 Apr 2022 20:07:06 +0530 Subject: [PATCH] Initialize project in EditorActivity using Tooling API --- .idea/misc.xml | 2 + app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 13 +- .../assets/data/common/tooling-api-all.jar | Bin 4791217 -> 4791217 bytes .../com/itsaky/androidide/EditorActivity.java | 1930 +++++++++-------- .../services/GradleBuildService.java | 233 ++ .../itsaky/androidide/utils/JSONUtility.java | 15 +- .../res/mipmap-hdpi/ic_launcher_round.png | Bin 5339 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 3388 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 7472 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 11873 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 16570 -> 0 bytes app/src/main/res/values/strings.xml | 4 + .../itsaky/androidide/builder/BuildService.kt | 2 +- build-tools/tooling-api-impl/build.gradle | 1 - .../test/test-home/.androidide/init/model.jar | Bin 54335 -> 54335 bytes build-tools/tooling-api/build.gradle | 4 +- .../androidide/app/BaseApplication.java | 67 +- common/src/main/res/values/strings.xml | 3 +- 19 files changed, 1274 insertions(+), 1001 deletions(-) create mode 100644 app/src/main/java/com/itsaky/androidide/services/GradleBuildService.java delete mode 100755 app/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100755 app/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100755 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100755 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100755 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png 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 eff3804be16a0ef60dafd2bb427f8946f5ea62ba..40c3d54eb654b355fc51e4efbd140f78991b3676 100644 GIT binary patch delta 8211 zcmZ7)33L=yl5|g!xiXm~5J;GW95Vxf92^M=5OYj7-3ft!0s?X)M>xYNq6iV$4X%qu z#griDig@4xk(Cc9hq3~?3kjge8SZe%DIk}dY*oz=qVwLL?mB+`s`^#c^?!v}Emh#K z_9~DK=0=T-#zvLPR$04AmCN3>b<4rW#$Me-b9$Oa7yHxmrSZ>&5_xt&+jm#4GHw=q998OBn*JR-U1IqCh><`7h#WQGrpNLHHl=@BW#K&0SHQUnxSNif0hCzu2|7ZO|| z<)pNcl!Wsar39LQd=}CK99N`7dZNq-_%vCuisCEMEtycu-JK)gdO6O{s4z&fSgo6)kSc2wq{PWK@nMYo54M*%2?ZNm zvcH(=l79>!2{C<5QLsK)4i_bf@}j7Ep{IJvVM6OB?s3% zu`+vf`B=HPg-FB3TBC%0hWrKlj~Um`L&m1U-!1}5gN*k6|`S`jLj zj5u8(Q=G9BKq*GJ`IT&i{(4@(qdQlN8&gUGnc7E4AOr`=d!l z-3&t_lucGlLcSokVlz)w8+d*o<6-?&B#!?@evvh2;W0Cm5Yhae+?mx!SxM!yl~6z? zyIA9A=*F=|HhQ%yi#J$Bd=o=hy)G~;&0zEL&vO~}P*w=kaSfR*Za-~!ojIRKI6Vd> ztRG>x#0ru%uydkM0^9|~4nJ=*gu%`!2B&B}&TxoS6=>RY2icf3ANQ?zsUd_`hl2o>aKX+G4JKh1{;PRgaq(oRx*cE=*LL_{=t#3sun%Rq z=WpsqC{!pNhW)BoME}A7eI=sAs_OwaBYd>ZP!=6%6h9dQ`=$^L+3#(ypEtQS&?3g% z3oK+&7&I-w-qRQaf5|u4p?`{Mhdrws2MNUGvp|e&FGqO?HBh`+WQ6G?tK~^!5bb%c0@+JVCb4q9Y02YMhyuo^3_veT z`OtKn%C{)oWJ=?@z@`oDXAXd*!MN6jt)|tCNB_FZN2vku?<%t$OheGU0e`bLCY1Pk zhv~PGv?}|q&Sef2=bYxXlp&%l!yHD5EI#UO9!bSWpdo*qMcf$rZrFLvY=OUz3AKwK ztIUPH3C~{0DY6*FGfvAVHIG?EoW=UBlOU{JZrJd?Y8K&bt+7oBg+kdblNhR7>*zZp zQ2gb`K5;|IDzs+SH0w@|ZM4!|zRBt@Mz67cnn2u$S;2cil0;>-)y5?m%NEXEr&>jb zDQL%VT8h<+iLe>V#|KfO!OcW_G7S4Y$N)J$Oxq+c#`32)fcM4+^`I$B$HnuMFyWdI zbUB6;z?GKwqL&RPgAQLJrpz&+$*}SVHBKDaZ9CbMP*s6;4TcR1_7_sq;Eevno>?6H zu!z}$uW9Y=`Rml_($?n6WlRDB=n56NRf$p@vVD~>RcIi_(erAq10)s1m9!4Zz5DUN11 zB5*|FXpX~yBML_}jutp##L(5TW2;Cdk}u3N;(mQ>bTX8Etwo9a@8bSCn-~t{#^=~7 zmo4kl2!9g&HG;QqSt5b<1#)(3L88zTAI@J8v=hUuAl?GigX4{2#o+is9MM>{uwo`z zLSWclEl{){5#N(5FP8c!SXmTLnJ%h|;(ws@6S!m3g6IV^*glDLz`_wFa%$p#=}YLO zOy_a_)~eTBt%!`ZB3#VB=2}l(0K|Oc2^R&D`{k*`ymz|$pRB{oS#B;qYi7CAiEwI` zdkug5%{fkvKbi;KbDWR2GF;5baek?gNQ`e3Ip5iw%9beF>O8Z6&}ddTJXqoihq>Kd zCQ+iehI4@=yg>Xy-h8YN;bQqw*J}QZmCki@5_~b&9ZxAJem>|<(CI~rZ22Ef-^#R> z(St}nwpZcs%TBkoJXQ|@%qXi!ey8Pt65@V6wAHBr_)I4tVIp~QMWz`Am$ms%`t)rZ*&2J>I`>2N!LvAFjQ;8WnqzKV6we=R>A#AB4 zN*{G@jiZft)H%_%me#b?B;rWIk&Gh+M_Xa3Y4`0xl3%&G{m08l629QKYGMad>6&Ld zh&9iq?#?ATc5#iz6CI^Qh-Gq`onwdEH2BX8xn^-NI<1o1IA-`dJ#7jFfk;`CHk@`+ zyz^Pwi~Rkto=Hn3K}d0e{dQUqe~`u8Giepw3D3$GBzF`A$>{^g>wqRlh#mdXXJ-31 ziZ~ zv}ZpF-TuAi5Z|2f+#qFO87L01|D5M6tMv@mr8dSv~&qzt>Pu&(&j8YwNEJ%5yp7fec z`H~s0O-B`;ZLT~;(co36qJ`3&O9BhI5~nzfL^HNgI=dO1 z0c4z*ouZ^Y@j_)gg}tz|J&H56S6*gu; z<7vYRsaLp0ktcYMAA6>9Il_YN*Tco{O6x2;jKQxfRZaF?!{gHPPZT=U}$U5q2Cgv8YvsoHe0|!J* zP`~x5BOXjp7jsl_=xms()=^y*6|blv;UtyOr{7fzX@5l9_tjXEN|uP?C2A21WuCjs zR8zev!m&dAhT|7iJkDFo&OP#}>L#X5YgOOz`k(4?jt36Pd&27hPSD)VD%S<7-SF0C zHJ#MiyICDbPcGfLThv%ylJm4=tGb+Oqlnq19_D`F{`cxRR{P|B^*m#C{iJT-ml-G2 zovb^O9HRGWbt4Pn%yyntDYHedv+CE3wVhYH)1s&eH8y~o@EkD&T~K?HH^h*Os)vd# z;0wk~d{>14T;3sCIJ+M6AXNHjbXW0cw3gTHF`pP-p-pAiunxDr)W)$V_`@}8hsGZ+ z&b;q;XxY3+{26n9r#V?I&g!DwS`_I3rF%8%n&9kS?HE1?^`1^BmGvHSr>C7zR_jUvN0GuwOy2|fMjt1% zGw4srv3;E|#h@3;#|9$C>?QkMgpOC~sfc6Oa2G@+bf;KRRZ(o^MkmpS1|H+KuK5AK+7;(+B&2r=vVa^VUm3^wbz^5j)c zSY_7pec8_s4+=w|_ozT{TT$i>vA$vUP-9V%9e_-9P-3i+*dV&1@ zPA6EcI#)#M4CHxxoY2FH8@~ZxUbpJc$d>P&P)#on@TJ#GcPkhegl1g)0k{1{RD6HB z&YMitIJVt?osD{Q?US37U#O)_9R9+nNh!Sw{G`T>9^Vaf-Ia&^7wxd=Zi+sNYGBFwFsL-biKLfZFR1BR-RxYYQwsL8Yk_2|Z= z8&5Gn>(`ui>yzvp+5}eJLJM#`D-3(h83c(Vyh%xgT$&J0Xs$v`A@DQ!n3}e{nX0${{{d!FeQgjQ5h-zU_>IwSDuvv$hgxe#?4NBwUQu zLs?+U3rHG~3E3L5E}^uG zK4AIx&L&W|758Udo#NGG&n47E^zkw?z4BXU0IZ16z4VOBNdI7PgLDXSyDV+Yl`+oLi5LhL|fF-qGFoL+E6^>Sfq|P1B{1=hp+l zkaHb#$u>&H@zg=4XsexT~-f-!>L-IlVHOQ)S-Bhmp1vP7D{f& zkcL7%#v^rIUk`-B&6`@fWUe4gme*wqVqFbGecMK&O_G{^4I#g3FG;oE(6bwmZgBHg zTyD?53DX4%ZlnI)b`yIWC`@%ZVObmBzMZ&@`mg(;o@9hJceK#QbxNA|6Hy#|Qv&nu zpx?i1h&glzb<8~K<;m{lvVgq^|FZ7ldVi~~pBe}2?&8#LXX_cG2xEsMz5ifQP)+?5 z6IlAVB8bZhum6U&oWIh5w*NP+qjdImy*OSs04@!8VFum9rTX1#z} zJ(OgB_{Vuoz;Yj_w5)9~Wzc=Cle|}QgWm*K0`#^i-hBP}z7{FZGPt4LD0E`82U@1w z7w-lcLY)HO)dyNTd5_Hvt7f}8mZ$jY#-j%~?N*o@ay`!Guy;N-8^6Dbe?uQ?t>r`E zPw;p+J)?OO>d-@+6A;gm;vn>q)=|!M`=E)Bv12lK*3ahAMc?Y zndFAy%TV7pf5+tpd!Y_+{dcXiT$;kPt}yw3NQ-I5Pdu<@8Y(F;sI?uvoBA223%`{~h+4`#a?4IfcFV|sGY z^obMx^d-b2H+R+4?TPYWy*;&v+}qbQ(T{UmNp}k3R5QK7(K|9y{}(sWcm=aA z_KIZvDL%GwU-~djPv@&HJ`VLP)UR;+Nk>NL*H`D;EpBchB`7)-7&8l1+PHYT;+2wtDo8@?}b>B$V_`@(#;dst*XT}lhw<2b+n0}PbQ=>Px# delta 8404 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 SeDNO~nMYr-1s-p_*Z%>s(yJx_ 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 61da551c5594a1f9d26193983d2cd69189014603..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5339 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 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 532157257d67bf6e0bbde53fb6051678db8723e9..ec7cf51f7729e8203a96b5cc472a2e44d1cbdddd 100644 GIT binary patch delta 31 ncmdnLf_eW6W}X0VW)?061`ZB}jSDvNxE*EMxL|Y0(bs$cneYnE delta 31 ncmdnLf_eW6W}X0VW)?061`ZAe(f*A*Zbz9!`!|Oiea!~|i{S}$ 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…