Merged branch 'tooling-api' into 'main'

This commit is contained in:
Akash Yadav
2022-05-08 23:35:34 +05:30
236 changed files with 7464 additions and 5816 deletions
+3 -1
View File
@@ -2,7 +2,9 @@ name: Android CI
on:
push:
branches: [ main ]
branches:
- main
- tooling-api
jobs:
build:
+2
View File
@@ -11,6 +11,8 @@
</profile>
</annotationProcessing>
<bytecodeTargetLevel target="1.8">
<module name="AndroidIDE.build-tools.builder-model" target="11" />
<module name="AndroidIDE.build-tools.project" target="11" />
<module name="AndroidIDE.build-tools.tooling-api-plugin" target="11" />
<module name="AndroidIDE.google-java-format" target="1.8" />
<module name="AndroidIDE.gradle-tooling" target="1.8" />
+2
View File
@@ -86,6 +86,8 @@
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/layout/test.xml" value="0.20512820512820512" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/menu/menu_editor.xml" value="0.20512820512820512" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/menu/menu_editor_nav.xml" value="0.20512820512820512" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" value="0.1445" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml" value="0.1445" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/common/src/main/res/layout/layout_color_picker.xml" value="0.264" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/xml-inflater/src/main/res/drawable/empy_drawable.xml" value="0.147" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/xml-inflater/src/main/res/drawable/states.xml" value="0.147" />
@@ -56,8 +56,34 @@ interface ActionItem {
*/
fun postExec(data: ActionData, result: Any) = Unit
/**
* Return the show as action flags for the menu item.
*
* @return The show as action flags.
*/
fun getShowAsActionFlags(data: ActionData): Int = -1
/**
* Checks if the given [ActionData] has instances of the given [types].
*
* @param data The data to check.
* @param types The type of objects to look for.
* @return `true` if the [data] has the given [types], `false` otherwise.
*/
fun hasRequiredData(data: ActionData, vararg types: Class<*>): Boolean {
for (type in types) {
data.get(type) ?: return false
}
return true
}
/** Marks this action item as invisible. */
fun markInvisible() {
visible = false
enabled = false
}
/** Location where an action item will be shown. */
enum class Location(val id: String) {
@@ -24,7 +24,7 @@ import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionMenu
import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.utils.ILogger
import java.util.concurrent.CompletableFuture
import java.util.concurrent.*
/**
* Default implementation for the [ActionsRegistry]
@@ -148,21 +148,22 @@ class DefaultActionsRegistry : ActionsRegistry() {
private fun execInBackground(action: ActionItem, data: ActionData, it: MenuItem) {
val start = System.currentTimeMillis()
CompletableFuture.supplyAsync { action.execAction(data) }.whenComplete { result, error ->
if (result == null || (result is Boolean && !result) || error != null) {
log.error(
"An error occurred when performing action '${it.title}'. Action failed in ${System.currentTimeMillis() - start}ms",
error)
} else {
log.info(
"Action '${it.title}' completed in ${System.currentTimeMillis() - start}ms")
}
CompletableFuture.supplyAsync { action.execAction(data) }
.whenComplete { result, error ->
if (result == null || (result is Boolean && !result) || error != null) {
log.error(
"An error occurred when performing action '${it.title}'. Action failed in ${System.currentTimeMillis() - start}ms",
error)
} else {
log.info(
"Action '${it.title}' completed in ${System.currentTimeMillis() - start}ms")
}
ThreadUtils.runOnUiThread {
action.postExec(data, result ?: false)
notifyActionExec(action, result ?: false)
ThreadUtils.runOnUiThread {
action.postExec(data, result ?: false)
notifyActionExec(action, result ?: false)
}
}
}
}
private fun notifyActionExec(action: ActionItem, result: Any) {
+39 -34
View File
@@ -4,60 +4,60 @@ apply plugin: 'kotlin-android'
android {
compileSdkVersion rootProject.ext.compileSdk
buildToolsVersion rootProject.ext.buildTools
defaultConfig {
applicationId rootProject.ext.packageName
minSdkVersion rootProject.ext.minSdk
targetSdkVersion rootProject.ext.targetSdk
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility rootProject.ext.javaSourceVersion
targetCompatibility rootProject.ext.javaTargetVersion
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
viewBinding true
}
signingConfigs {
debug {
storeFile file("dev.keystore")
storeFile file ("dev.keystore")
keyAlias "androidide"
storePassword "ed68424fb109e5aa8146e4b86caa72e3"
keyPassword "ed68424fb109e5aa8146e4b86caa72e3"
}
release {
storeFile file("dev.keystore")
storeFile file ("dev.keystore")
keyAlias "androidide"
storePassword "ed68424fb109e5aa8146e4b86caa72e3"
keyPassword "ed68424fb109e5aa8146e4b86caa72e3"
}
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile ('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
}
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile ('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
@@ -66,7 +66,7 @@ android {
excludes += ['META-INF/eclipse.inf', 'about_files/LICENSE-2.0.txt', 'plugin.xml', 'plugin.properties', 'about.mappings', 'about.properties', 'about.ini', 'modeling32.png']
}
}
lint {
abortOnError false
disable 'VectorPath', 'NestedWeights', 'ContentDescription', 'SmallSp'
@@ -74,10 +74,10 @@ android {
}
dependencies {
// Annotation processors
annotationProcessor 'com.github.bumptech.glide:compiler:4.13.1'
implementation common.editor
implementation common.utilcode
implementation common.file_picker
@@ -87,9 +87,9 @@ dependencies {
implementation common.xml2json
implementation google.gson
implementation google.guava
implementation common.jsoup
// AndroidX
implementation androidx.annotation
implementation androidx.appcompat
@@ -103,24 +103,29 @@ dependencies {
implementation androidx.vectors
implementation androidx.animated_vectors
implementation google.material
// Kotlin
implementation "androidx.core:core-ktx:1.7.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Local projects here
implementation project(':treeview')
implementation project(':emulatorview')
implementation project(':xml-inflater')
implementation project(':lexers')
implementation project(':common')
implementation project(':actions')
implementation project(':lsp:api')
implementation project(':lsp:java')
implementation project(':lsp:xml')
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project (':build-tools:builder-api')
implementation project (':treeview')
implementation project (':emulatorview')
implementation project (':xml-inflater')
implementation project (':lexers')
implementation project (':common')
implementation project (':actions')
implementation project (':lsp:api')
implementation project (':lsp:java')
implementation project (':lsp:xml')
implementation fileTree (dir: 'libs', include: ['*.jar'])
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
// This is to build the tooling-api-impl project before the app is built
// So we always copy the latest JAR file to assets
compileOnly project (path: ':build-tools:tooling-api-impl')
}
+3 -10
View File
@@ -68,6 +68,9 @@
android:configChanges="orientation|screenSize|uiMode"
android:windowSoftInputMode="adjustResize" />
<service
android:name=".services.GradleBuildService"
android:exported="false" />
<provider
android:name=".filepicker.IDEDocumentsProvider"
android:authorities="com.itsaky.androidide.documents"
@@ -87,16 +90,6 @@
<action android:name="com.itsaky.androidide.logs.APPEND_LOG" />
</intent-filter>
</receiver>
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@mipmap/ic_launcher" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/secondaryColor" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/cms_channel_id_devs" />
</application>
</manifest>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -39,18 +39,15 @@ import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.models.NewProjectDetails;
import com.itsaky.androidide.models.ProjectTemplate;
import com.itsaky.androidide.project.AndroidProject;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.callables.ProjectCreatorCallable;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.ProjectFinder;
import com.itsaky.androidide.utils.TransformUtils;
import com.itsaky.toaster.Toaster;
import java.io.File;
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.Callable;
import abhishekti7.unicorn.filepicker.UnicornFilePicker;
@@ -68,11 +65,213 @@ public class MainActivity extends StudioActivity
private int currentTemplateIndex = 0;
@Override
protected View bindLayout() {
binding = ActivityMainBinding.inflate(getLayoutInflater());
createBinding = binding.createLayout;
createLayoutBinding = createBinding.createProjectContent;
return binding.getRoot();
public void onClick(View p1) {
if (p1.getId() == binding.createProject.getId()) {
showCreateProject();
} else if (p1.getId() == createBinding.createprojectClose.getId()) {
hideCreateProject();
} else if (p1.getId() == createLayoutBinding.nextCard.getId()) {
currentTemplateIndex++;
if (currentTemplateIndex >= mTemplates.size()) {
currentTemplateIndex = 0;
}
showTemplate(currentTemplateIndex);
} else if (p1.getId() == createLayoutBinding.previousCard.getId()) {
currentTemplateIndex--;
if (currentTemplateIndex < 0) {
currentTemplateIndex = mTemplates.size() - 1;
}
showTemplate(currentTemplateIndex);
} else if (p1.getId() == createLayoutBinding.createprojectCreate.getId()) {
createNewProject();
} else if (p1.getId() == binding.gotoPreferences.getId()) {
gotoSettings();
} else if (p1.getId() == binding.openProject.getId()) {
pickProject();
} else if (p1.getId() == binding.openTerminal.getId()) {
startActivity(new Intent(this, TerminalActivity.class));
}
}
private void showCreateProject() {
MaterialContainerTransform transform =
TransformUtils.createContainerTransformFor(
binding.createProject, binding.createNewCard, binding.realContainer);
TransitionManager.beginDelayedTransition(binding.getRoot(), transform);
binding.createNewCard.setVisibility(View.VISIBLE);
}
private void hideCreateProject() {
MaterialContainerTransform transform =
TransformUtils.createContainerTransformFor(
binding.createNewCard, binding.createProject, binding.realContainer);
TransitionManager.beginDelayedTransition(binding.getRoot(), transform);
binding.createNewCard.setVisibility(View.GONE);
}
private void showTemplate(int index) {
if (index < 0 || index >= mTemplates.size()) {
return;
}
final ProjectTemplate template = mTemplates.get(index);
createLayoutBinding.createprojectTemplateLabel.setText(template.getName());
createLayoutBinding.createprojectTemplateDescription.setText(template.getDescription());
createLayoutBinding.createprojectTemplateImage.setImageResource(template.getImageId());
}
private void createNewProject() {
final String appName =
Objects.requireNonNull(createLayoutBinding.createprojectTextAppName.getEditText())
.getText()
.toString()
.trim();
final String packageName =
Objects.requireNonNull(
createLayoutBinding.createprojectTextPackageName.getEditText())
.getText()
.toString()
.trim();
final int minSdk = getMinSdk();
final int targetSdk = getTargetSdk();
if (!isValid(appName, packageName, minSdk, targetSdk)) {
getApp().toast(R.string.invalid_values, Toaster.Type.ERROR);
return;
}
createProject(appName, packageName, minSdk, targetSdk);
}
private void gotoSettings() {
startActivity(new Intent(this, PreferencesActivity.class));
}
private void pickProject() {
UnicornFilePicker.from(this)
.addConfigBuilder()
.addItemDivider(false)
.selectMultipleFiles(false)
.setRootDirectory(Environment.getExternalStorageDirectory().getAbsolutePath())
.showHiddenFiles(true)
.showOnlyDirectory(true)
.theme(R.style.AppTheme_FilePicker)
.build()
.forResult(abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE);
}
private int getMinSdk() {
try {
return Integer.parseInt(
Objects.requireNonNull(
createLayoutBinding.createprojectTextMinSdk.getEditText())
.getText()
.toString());
} catch (Exception e) {
getApp().toast(e.getMessage(), Toaster.Type.ERROR);
}
return -1;
}
private int getTargetSdk() {
try {
return Integer.parseInt(
Objects.requireNonNull(
createLayoutBinding.createprojectTextTargetSdk.getEditText())
.getText()
.toString());
} catch (Exception e) {
getApp().toast(e.getMessage(), Toaster.Type.ERROR);
}
return -1;
}
private boolean isValid(String appName, String packageName, int minSdk, int targetSdk) {
return appName != null
&& appName.length() > 0
&& packageName != null
&& packageName.length() > 0
&& minSdk > 1
&& minSdk < 99
&& targetSdk > 1
&& targetSdk < 99;
}
private void createProject(String appName, String packageName, int minSdk, int targetSdk) {
new TaskExecutor()
.executeAsync(
new ProjectCreatorCallable(
mTemplates.get(currentTemplateIndex),
new NewProjectDetails(appName, packageName, minSdk, targetSdk),
this),
r -> {});
}
@Override
public void beforeBegin() {
if (mProgressSheet == null) {
createProgressSheet();
}
setMessage(R.string.msg_begin_project_write);
}
@Override
public void onProcessTask(String taskName) {
setMessage(taskName);
}
@Override
public void onSuccess(File root) {
if (mProgressSheet == null) {
createProgressSheet();
}
if (mProgressSheet.isShowing()) {
mProgressSheet.dismiss();
}
getApp().toast(R.string.project_created_successfully, Toaster.Type.SUCCESS);
openProject(root);
}
@Override
public void onFailed(String reason) {
if (mProgressSheet == null) {
createProgressSheet();
}
if (mProgressSheet.isShowing()) {
mProgressSheet.dismiss();
}
getApp().toast(reason, Toaster.Type.ERROR);
}
private void openProject(File root) {
final var intent = new Intent(this, EditorActivity.class);
intent.putExtra(EditorActivity.EXTRA_PROJECT_PATH, root.getAbsolutePath());
startActivity(intent);
}
private void createProgressSheet() {
mProgressSheet = new ProgressSheet();
mProgressSheet.setShowShadow(false);
}
private void setMessage(int msg) {
setMessage(getString(msg));
}
private void setMessage(String msg) {
mProgressSheet.setMessage(msg);
}
@Override
public void onBackPressed() {
if (binding.createNewCard.getVisibility() == View.VISIBLE) {
hideCreateProject();
} else {
super.onBackPressed();
}
}
@Override
@@ -127,20 +326,6 @@ public class MainActivity extends StudioActivity
});
}
private void askProjectOpenPermission(File root) {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(R.string.title_confirm_open_project);
builder.setMessage(getString(R.string.msg_confirm_open_project, root.getAbsolutePath()));
builder.setCancelable(false);
builder.setPositiveButton(R.string.yes, (d, w) -> openProject(root));
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private void gotoSettings() {
startActivity(new Intent(this, PreferencesActivity.class));
}
@Override
protected void onStorageGranted() {}
@@ -151,120 +336,11 @@ public class MainActivity extends StudioActivity
}
@Override
public void onClick(View p1) {
if (p1.getId() == binding.createProject.getId()) {
showCreateProject();
} else if (p1.getId() == createBinding.createprojectClose.getId()) {
hideCreateProject();
} else if (p1.getId() == createLayoutBinding.nextCard.getId()) {
currentTemplateIndex++;
if (currentTemplateIndex >= mTemplates.size()) {
currentTemplateIndex = 0;
}
showTemplate(currentTemplateIndex);
} else if (p1.getId() == createLayoutBinding.previousCard.getId()) {
currentTemplateIndex--;
if (currentTemplateIndex < 0) {
currentTemplateIndex = mTemplates.size() - 1;
}
showTemplate(currentTemplateIndex);
} else if (p1.getId() == createLayoutBinding.createprojectCreate.getId()) {
createNewProject();
} else if (p1.getId() == binding.gotoPreferences.getId()) {
gotoSettings();
} else if (p1.getId() == binding.openProject.getId()) {
pickProject();
} else if (p1.getId() == binding.openTerminal.getId()) {
startActivity(new Intent(this, TerminalActivity.class));
}
}
@Override
public void beforeBegin() {
if (mProgressSheet == null) {
createProgressSheet();
}
setMessage(R.string.msg_begin_project_write);
}
@Override
public void onProcessTask(String taskName) {
setMessage(taskName);
}
@Override
public void onSuccess(File root) {
if (mProgressSheet == null) {
createProgressSheet();
}
if (mProgressSheet.isShowing()) {
mProgressSheet.dismiss();
}
getApp().toast(R.string.project_created_successfully, Toaster.Type.SUCCESS);
openProject(root);
}
@Override
public void onFailed(String reason) {
if (mProgressSheet == null) {
createProgressSheet();
}
if (mProgressSheet.isShowing()) {
mProgressSheet.dismiss();
}
getApp().toast(reason, Toaster.Type.ERROR);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE
&& resultCode == RESULT_OK) {
ArrayList<String> files = data.getStringArrayListExtra("filePaths");
if (files != null) {
if (files.size() == 1) {
File choseDir = new File(files.get(0));
if (choseDir.exists() && choseDir.isDirectory()) {
openProject(choseDir);
} else {
getApp().toast(R.string.msg_picked_isnt_dir, Toaster.Type.ERROR);
}
} else {
getApp().toast(R.string.msg_pick_single_file, Toaster.Type.ERROR);
}
}
}
}
@Override
public void onBackPressed() {
if (binding.createNewCard.getVisibility() == View.VISIBLE) {
hideCreateProject();
} else {
super.onBackPressed();
}
}
@Override
protected void onResume() {
super.onResume();
if (mProgressSheet != null && mProgressSheet.isShowing()) {
mProgressSheet.dismiss();
}
}
private void showTemplate(int index) {
if (index < 0 || index >= mTemplates.size()) {
return;
}
final ProjectTemplate template = mTemplates.get(index);
createLayoutBinding.createprojectTemplateLabel.setText(template.getName());
createLayoutBinding.createprojectTemplateDescription.setText(template.getDescription());
createLayoutBinding.createprojectTemplateImage.setImageResource(template.getImageId());
protected View bindLayout() {
binding = ActivityMainBinding.inflate(getLayoutInflater());
createBinding = binding.createLayout;
createLayoutBinding = createBinding.createProjectContent;
return binding.getRoot();
}
/** Must be called in/after onViewCreated */
@@ -310,147 +386,42 @@ public class MainActivity extends StudioActivity
mTemplates.add(libgdx);
}
private void createProgressSheet() {
mProgressSheet = new ProgressSheet();
mProgressSheet.setShowShadow(false);
private void askProjectOpenPermission(File root) {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(R.string.title_confirm_open_project);
builder.setMessage(getString(R.string.msg_confirm_open_project, root.getAbsolutePath()));
builder.setCancelable(false);
builder.setPositiveButton(R.string.yes, (d, w) -> openProject(root));
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private void setMessage(int msg) {
setMessage(getString(msg));
}
private void setMessage(String msg) {
mProgressSheet.setMessage(msg);
}
private void openProject(File root) {
setMessage(R.string.msg_opening_project);
mProgressSheet.setCancelable(false);
mProgressSheet.show(getSupportFragmentManager(), "opening_project");
new TaskExecutor().executeAsync(new ProjectReader(root), this::openEditor);
}
private void pickProject() {
UnicornFilePicker.from(this)
.addConfigBuilder()
.addItemDivider(false)
.selectMultipleFiles(false)
.setRootDirectory(Environment.getExternalStorageDirectory().getAbsolutePath())
.showHiddenFiles(true)
.showOnlyDirectory(true)
.theme(R.style.AppTheme_FilePicker)
.build()
.forResult(abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE);
}
private void openEditor(AndroidProject project) {
if (project == null) {
getApp().toast(R.string.msg_invalid_project, Toaster.Type.ERROR);
} else {
startActivity(
new Intent(this, EditorActivity.class)
.putExtra(EditorActivity.EXTRA_PROJECT, project));
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE
&& resultCode == RESULT_OK) {
ArrayList<String> files = data.getStringArrayListExtra("filePaths");
if (files != null) {
if (files.size() == 1) {
File choseDir = new File(files.get(0));
if (choseDir.exists() && choseDir.isDirectory()) {
openProject(choseDir);
} else {
getApp().toast(R.string.msg_picked_isnt_dir, Toaster.Type.ERROR);
}
} else {
getApp().toast(R.string.msg_pick_single_file, Toaster.Type.ERROR);
}
}
}
}
private void createNewProject() {
final String appName =
Objects.requireNonNull(createLayoutBinding.createprojectTextAppName.getEditText())
.getText()
.toString()
.trim();
final String packageName =
Objects.requireNonNull(
createLayoutBinding.createprojectTextPackageName.getEditText())
.getText()
.toString()
.trim();
final int minSdk = getMinSdk();
final int targetSdk = getTargetSdk();
if (!isValid(appName, packageName, minSdk, targetSdk)) {
getApp().toast(R.string.invalid_values, Toaster.Type.ERROR);
return;
}
createProject(appName, packageName, minSdk, targetSdk);
}
private void createProject(String appName, String packageName, int minSdk, int targetSdk) {
new TaskExecutor()
.executeAsync(
new ProjectCreatorCallable(
mTemplates.get(currentTemplateIndex),
new NewProjectDetails(appName, packageName, minSdk, targetSdk),
this),
r -> {});
}
private boolean isValid(String appName, String packageName, int minSdk, int targetSdk) {
return appName != null
&& appName.length() > 0
&& packageName != null
&& packageName.length() > 0
&& minSdk > 1
&& minSdk < 99
&& targetSdk > 1
&& targetSdk < 99;
}
private int getMinSdk() {
try {
return Integer.parseInt(
Objects.requireNonNull(
createLayoutBinding.createprojectTextMinSdk.getEditText())
.getText()
.toString());
} catch (Exception e) {
getApp().toast(e.getMessage(), Toaster.Type.ERROR);
}
return -1;
}
private int getTargetSdk() {
try {
return Integer.parseInt(
Objects.requireNonNull(
createLayoutBinding.createprojectTextTargetSdk.getEditText())
.getText()
.toString());
} catch (Exception e) {
getApp().toast(e.getMessage(), Toaster.Type.ERROR);
}
return -1;
}
private void showCreateProject() {
MaterialContainerTransform transform =
TransformUtils.createContainerTransformFor(
binding.createProject, binding.createNewCard, binding.realContainer);
TransitionManager.beginDelayedTransition(binding.getRoot(), transform);
binding.createNewCard.setVisibility(View.VISIBLE);
}
private void hideCreateProject() {
MaterialContainerTransform transform =
TransformUtils.createContainerTransformFor(
binding.createNewCard, binding.createProject, binding.realContainer);
TransitionManager.beginDelayedTransition(binding.getRoot(), transform);
binding.createNewCard.setVisibility(View.GONE);
}
private static final class ProjectReader implements Callable<AndroidProject> {
private final File root;
public ProjectReader(File root) {
this.root = root;
}
@Override
public AndroidProject call() throws Exception {
return ProjectFinder.fromFolder(root);
@Override
protected void onResume() {
super.onResume();
if (mProgressSheet != null && mProgressSheet.isShowing()) {
mProgressSheet.dismiss();
}
}
}
@@ -33,25 +33,14 @@ import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.utils.DialogUtils;
public class SplashActivity extends StudioActivity {
private ActivitySplashBinding binding;
private static final Handler mHandler = new Handler();
private final Runnable mRunnable =
new Runnable() {
@Override
public void run() {
ConstantsBridge.SPLASH_TO_MAIN = true;
startActivity(new Intent(SplashActivity.this, MainActivity.class));
finish();
}
() -> {
ConstantsBridge.SPLASH_TO_MAIN = true;
startActivity(new Intent(SplashActivity.this, MainActivity.class));
finish();
};
@Override
protected View bindLayout() {
binding = ActivitySplashBinding.inflate(getLayoutInflater());
return binding.getRoot();
}
private ActivitySplashBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -59,6 +48,12 @@ public class SplashActivity extends StudioActivity {
binding.splashText.setText(getString(R.string.msg_checking_storage_permissions));
}
@Override
protected void onStorageAlreadyGranted() {
super.onStorageAlreadyGranted();
proceed();
}
@Override
protected void onStorageGranted() {
super.onStorageGranted();
@@ -66,9 +61,9 @@ public class SplashActivity extends StudioActivity {
}
@Override
protected void onStorageAlreadyGranted() {
super.onStorageAlreadyGranted();
proceed();
protected View bindLayout() {
binding = ActivitySplashBinding.inflate(getLayoutInflater());
return binding.getRoot();
}
private void proceed() {
@@ -42,7 +42,7 @@ abstract class BaseBuildAction : EditorActivityAction() {
enabled = true
}
}
override fun postExec(data: ActionData, result: Any) {
val context = getActivity(data) ?: return
context.invalidateOptionsMenu()
@@ -51,5 +51,5 @@ abstract class BaseBuildAction : EditorActivityAction() {
fun shouldPrepare() = visible && enabled
private fun isBuildInProgress(activity: EditorActivity): Boolean =
activity.buildServiceHandler.isBuilding
activity.buildService == null || activity.buildService.isBuildInProgress
}
@@ -32,6 +32,12 @@ abstract class EditorActivityAction : ActionItem {
override var requiresUIThread: Boolean = false
override fun prepare(data: ActionData) {
if (!hasRequiredData(data, Context::class.java)) {
markInvisible()
}
}
fun getActivity(data: ActionData): EditorActivity? {
val context = data.get(Context::class.java) ?: return null
return context as EditorActivity
@@ -36,7 +36,7 @@ class AssembleDebugAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val activity = getActivity(data) ?: return false
activity.buildServiceHandler.assembleDebug(true)
activity.assembleDebug(true)
return true
}
}
@@ -36,7 +36,7 @@ class AssembleReleaseAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val activity = getActivity(data) ?: return false
activity.buildServiceHandler.service.assembleRelease()
activity.assembleRelease()
return true
}
}
@@ -36,7 +36,7 @@ class BuildAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val activity = getActivity(data) ?: return false
activity.buildServiceHandler.service.build()
activity.build()
return true
}
}
@@ -22,12 +22,15 @@ import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorActivityAction
import com.itsaky.androidide.utils.ILogger
/** @author Akash Yadav */
class StopDaemonsAction() : EditorActivityAction() {
class CancelBuildAction() : EditorActivityAction() {
private val log = ILogger.newInstance(javaClass.simpleName)
constructor(context: Context) : this() {
label = context.getString(R.string.stop_gradle_daemons)
label = context.getString(R.string.title_cancel_build)
icon = ContextCompat.getDrawable(context, R.drawable.ic_stop_daemons)
}
@@ -41,7 +44,23 @@ class StopDaemonsAction() : EditorActivityAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.stopAllDaemons()
log.info("Sending build cancellation request...")
context.buildService.cancelCurrentBuild().whenComplete { result, error ->
if (error != null) {
log.error("Failed to send build cancellation request", error)
return@whenComplete
}
if (!result.wasEnqueued) {
log.warn(
"Unable to enqueue cancellation request",
result.failureReason,
result.failureReason!!.message)
return@whenComplete
}
log.info("Build cancellation request was successfully enqueued...")
}
return true
}
}
@@ -35,7 +35,7 @@ class CleanAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.clean()
context.clean()
return true
}
}
@@ -35,7 +35,7 @@ class CreateAABAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.bundle()
context.bundle()
return true
}
}
@@ -39,7 +39,7 @@ class QuickRunAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.assembleDebug(true)
context.assembleDebug(true)
return true
}
}
@@ -35,7 +35,7 @@ class RebuildAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.cleanAndRebuild()
context.cleanAndRebuild()
return true
}
}
@@ -18,8 +18,6 @@
package com.itsaky.androidide.actions.editor
import android.content.Context
import android.content.res.TypedArray
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseEditorAction
@@ -23,6 +23,7 @@ import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorRelatedAction
import com.itsaky.androidide.app.StudioApp
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
import com.itsaky.toaster.Toaster
@@ -54,11 +55,19 @@ class SaveFileAction() : EditorRelatedAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
return try {
// Cannot use context.saveAll() because this.execAction is called on non-UI thread
// and saveAll call will result in UI actions
context.saveAllResult()
val result = context.saveAllResult()
if (result.xmlSaved) {
ProjectManager.generateSources(context.buildService)
}
if (result.gradleSaved) {
context.notifySyncNeeded()
}
true
} catch (error: Throwable) {
log.error("Failed to save file", error)
@@ -35,7 +35,7 @@ class LintAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.lint()
context.lint()
return true
}
}
@@ -35,7 +35,7 @@ class LintDebugAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.lintDebug()
context.lintDebug()
return true
}
}
@@ -35,7 +35,7 @@ class LintReleaseAction() : BaseBuildAction() {
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.lintRelease()
context.lintRelease()
return true
}
}
@@ -1,171 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.adapters;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.ChangeBounds;
import androidx.transition.TransitionManager;
import com.blankj.utilcode.util.FileUtils;
import com.bumptech.glide.Glide;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.LayoutProjectsItemBinding;
import com.itsaky.androidide.project.AndroidProject;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.toaster.Toaster;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class ProjectsListAdapter extends RecyclerView.Adapter<ProjectsListAdapter.VH> {
private ArrayList<AndroidProject> mProjects;
private OnProjectClickListener listener;
private final Comparator<AndroidProject> PROJECTS_SORTER =
new Comparator<AndroidProject>() {
@Override
public int compare(AndroidProject p1, AndroidProject p2) {
return p1.getAppName().compareTo(p2.getAppName());
}
};
public ProjectsListAdapter(
ArrayList<AndroidProject> mProjects, OnProjectClickListener listener) {
this.mProjects = mProjects;
this.listener = listener;
Collections.sort(this.mProjects, PROJECTS_SORTER);
}
@Override
public ProjectsListAdapter.VH onCreateViewHolder(ViewGroup p1, int p2) {
return new VH(
LayoutProjectsItemBinding.inflate(LayoutInflater.from(p1.getContext()), p1, false));
}
@Override
public void onBindViewHolder(ProjectsListAdapter.VH holder, int p2) {
final AndroidProject project = mProjects.get(p2);
holder.binding.projecsitemName.setText(project.getAppName());
holder.binding.projecsitemPkgName.setText(project.getPackageName());
holder.binding.deleteMsg.setText(
holder.binding
.deleteMsg
.getContext()
.getString(R.string.msg_confirm_delete, project.getAppName()));
if (!TextUtils.isEmpty(project.getIconPath()))
Glide.with(holder.itemView.getContext().getApplicationContext())
.load(new File(project.getIconPath()))
.into(holder.binding.projecsitemIcon);
else holder.binding.projecsitemIcon.setImageResource(R.drawable.ic_android);
holder.binding.projectsitemClickableContainer.setOnClickListener(
v -> sendClickEvent(project));
holder.binding.projectsitemClickableContainer.setOnLongClickListener(
v -> holder.toggleDelete());
holder.binding.delete.setOnClickListener(v -> holder.showConfirmDelete());
holder.binding.deleteCancel.setOnClickListener(v -> holder.hideAllDelete());
holder.binding.deleteOk.setOnClickListener(v -> deleteProject(project, p2));
}
private void deleteProject(AndroidProject project, int index) {
index = index >= mProjects.size() ? mProjects.size() - 1 : index;
if (index < 0) return;
mProjects.remove(index);
notifyItemRemoved(index);
new TaskExecutor()
.executeAsync(
() -> FileUtils.delete(project.getProjectPath()),
__ -> {
StudioApp.getInstance()
.toast(
__
? R.string.msg_project_deleted
: R.string.msg_project_delete_failed,
Toaster.Type.SUCCESS);
});
}
private void sendClickEvent(AndroidProject project) {
if (listener != null) listener.onProjectClick(project);
}
@Override
public int getItemCount() {
return mProjects.size();
}
public class VH extends RecyclerView.ViewHolder {
LayoutProjectsItemBinding binding;
public VH(LayoutProjectsItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void showConfirmDelete() {
binding.confirmDeleteContainer.setEnabled(true);
TransitionManager.beginDelayedTransition(
binding.getRoot(), new ChangeBounds().setDuration(85));
binding.confirmDeleteContainer.setVisibility(View.VISIBLE);
}
public void hideAllDelete() {
hideDelete();
hideConfirmDelete();
}
public boolean toggleDelete() {
binding.delete.setEnabled(!binding.delete.isEnabled());
TransitionManager.beginDelayedTransition(
binding.getRoot(), new ChangeBounds().setDuration(85));
binding.delete.setVisibility(binding.delete.isEnabled() ? View.VISIBLE : View.GONE);
return true;
}
public void hideDelete() {
binding.delete.setEnabled(false);
TransitionManager.beginDelayedTransition(
binding.getRoot(), new ChangeBounds().setDuration(85));
binding.delete.setVisibility(View.GONE);
}
public void hideConfirmDelete() {
binding.confirmDeleteContainer.setEnabled(false);
TransitionManager.beginDelayedTransition(
binding.getRoot(), new ChangeBounds().setDuration(85));
binding.confirmDeleteContainer.setVisibility(View.GONE);
}
}
public interface OnProjectClickListener {
public void onProjectClick(AndroidProject project);
}
}
@@ -24,7 +24,7 @@ import androidx.annotation.NonNull;
import com.blankj.utilcode.util.ThrowableUtils;
import com.itsaky.androidide.CrashHandlerActivity;
import com.itsaky.androidide.R;
import com.itsaky.androidide.project.ProjectResourceTable;
import com.itsaky.androidide.projects.ProjectResourceTable;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.apiinfo.ApiInfo;
import com.itsaky.attrinfo.AttrInfo;
@@ -66,16 +66,6 @@ public class StudioApp extends BaseApplication {
getApiInformation();
}
@NonNull
public ILanguageServer getJavaLanguageServer() {
return mJavaLanguageServer;
}
@NonNull
public ILanguageServer getXMLLanguageServer() {
return mXMLLanguageServer;
}
private void handleCrash(Thread thread, Throwable th) {
writeException(th);
@@ -95,6 +85,33 @@ public class StudioApp extends BaseApplication {
}
}
/** Reads API version information from api-versions.xml */
public CompletableFuture<SDKInfo> getApiInformation() {
return CompletableFuture.supplyAsync(
() -> {
if (sdkInfo == null) {
try {
sdkInfo = new SDKInfo(StudioApp.this);
((XMLLanguageServer) mXMLLanguageServer).setupSDK(sdkInfo);
} catch (Throwable th) {
LOG.error(getString(R.string.err_init_sdkinfo), th);
}
}
return sdkInfo;
});
}
@NonNull
public ILanguageServer getJavaLanguageServer() {
return mJavaLanguageServer;
}
@NonNull
public ILanguageServer getXMLLanguageServer() {
return mXMLLanguageServer;
}
public CompletableFuture<LayoutInflaterConfiguration> createInflaterConfig(
ILayoutInflater.ContextProvider contextProvider, Set<File> resDirs) {
@@ -119,27 +136,6 @@ public class StudioApp extends BaseApplication {
return mResTable == null ? mResTable = new ProjectResourceTable() : mResTable;
}
/** Reads API version information from api-versions.xml */
public CompletableFuture<SDKInfo> getApiInformation() {
return CompletableFuture.supplyAsync(
() -> {
if (sdkInfo == null) {
try {
sdkInfo = new SDKInfo(StudioApp.this);
((XMLLanguageServer) mXMLLanguageServer).setupSDK(sdkInfo);
} catch (Throwable th) {
LOG.error(getString(R.string.err_init_sdkinfo), th);
}
}
return sdkInfo;
});
}
public void stopAllDaemons() {
newShell(null).bgAppend("gradle --stop");
}
public ApiInfo apiInfo() {
return sdkInfo.getApiInfo();
}
@@ -40,7 +40,7 @@ import com.itsaky.androidide.R;
import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.LayoutEditorFileTreeBinding;
import com.itsaky.androidide.project.AndroidProject;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.callables.FileTreeCallable;
import com.itsaky.androidide.utils.Environment;
@@ -63,19 +63,27 @@ public class FileTreeFragment extends BottomSheetDialogFragment
private LayoutEditorFileTreeBinding binding;
private AndroidTreeView mFileTreeView;
private FileActionListener mFileActionListener;
private AndroidProject mProject;
private TreeNode mRoot;
private String mTreeState;
public FileTreeFragment() {}
@NonNull
public static FileTreeFragment newInstance(AndroidProject project) {
Bundle bundle = new Bundle();
bundle.putParcelable("project", project);
FileTreeFragment frag = new FileTreeFragment();
frag.setArguments(bundle);
return frag;
public static FileTreeFragment newInstance() {
return new FileTreeFragment();
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mFileActionListener = (FileActionListener) context;
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
saveTreeState();
outState.putString(KEY_STORED_TREE_STATE, mTreeState);
}
public void saveTreeState() {
@@ -87,49 +95,6 @@ public class FileTreeFragment extends BottomSheetDialogFragment
}
}
private void tryRestoreState() {
tryRestoreState(mTreeState);
}
private void tryRestoreState(String state) {
if (!TextUtils.isEmpty(state) && mFileTreeView != null) {
LOG.debug("Restoring tree view state:", "'" + state + "'");
mFileTreeView.collapseAll();
final var openNodesArray = state.split(NODES_PATH_SEPARATOR);
final var openNodes = new HashSet<>(Arrays.asList(openNodesArray));
restoreNodeState(mRoot, openNodes);
} else {
LOG.error(
"Unable to restore tree state",
"treeState=" + state,
"treeView=" + mFileTreeView);
}
}
private void restoreNodeState(@NonNull TreeNode root, Set<String> openNodes) {
final var children = root.getChildren();
for (int i = 0, childrenSize = children.size(); i < childrenSize; i++) {
final var node = children.get(i);
if (openNodes.contains(node.getPath())) {
listNode(
node,
() -> {
updateChevron(node);
expandNode(node);
restoreNodeState(node, openNodes);
});
}
}
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mFileActionListener = (FileActionListener) context;
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -140,7 +105,6 @@ public class FileTreeFragment extends BottomSheetDialogFragment
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mProject = requireArguments().getParcelable("project");
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_STORED_TREE_STATE)) {
mTreeState = savedInstanceState.getString(KEY_STORED_TREE_STATE, null);
@@ -188,11 +152,28 @@ public class FileTreeFragment extends BottomSheetDialogFragment
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
saveTreeState();
outState.putString(KEY_STORED_TREE_STATE, mTreeState);
public void collapseNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.collapseNode(node);
}
public void expandNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.expandNode(node);
}
private void setLoading(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).setLoading();
}
}
private void listNode(@NonNull TreeNode node, Runnable whenDone) {
@@ -222,6 +203,12 @@ public class FileTreeFragment extends BottomSheetDialogFragment
__ -> finalWhenDone.run());
}
private void updateChevron(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).updateChevron(!node.isExpanded());
}
}
private void getNodeFromFiles(File[] files, TreeNode parent) {
Arrays.sort(files, new FileTreeCallable.SortFileName());
Arrays.sort(files, new FileTreeCallable.SortFolder());
@@ -232,18 +219,6 @@ public class FileTreeFragment extends BottomSheetDialogFragment
}
}
private void setLoading(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).setLoading();
}
}
private void updateChevron(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).updateChevron(!node.isExpanded());
}
}
@Override
public boolean onLongClick(TreeNode node, Object value) {
if (mFileActionListener != null) {
@@ -252,32 +227,15 @@ public class FileTreeFragment extends BottomSheetDialogFragment
return true;
}
public void expandNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.expandNode(node);
}
public void collapseNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.collapseNode(node);
}
public void listProjectFiles() {
if (mProject == null) {
final var projectDirPath = ProjectManager.INSTANCE.getProjectDirPath ();
if (projectDirPath == null) {
return;
}
final File gradleProps = Environment.GRADLE_PROPS;
final File gradleHome = Environment.GRADLE_USER_HOME;
File projectDir = new File(mProject.getProjectPath());
File projectDir = new File(projectDirPath);
mRoot = TreeNode.root(projectDir);
if (gradleHome.exists() && gradleHome.isDirectory()) {
if (!gradleProps.exists()) {
@@ -342,6 +300,43 @@ public class FileTreeFragment extends BottomSheetDialogFragment
return binding.fileTreeLoadingProgress;
}
private void tryRestoreState() {
tryRestoreState(mTreeState);
}
private void tryRestoreState(String state) {
if (!TextUtils.isEmpty(state) && mFileTreeView != null) {
LOG.debug("Restoring tree view state:", "'" + state + "'");
mFileTreeView.collapseAll();
final var openNodesArray = state.split(NODES_PATH_SEPARATOR);
final var openNodes = new HashSet<>(Arrays.asList(openNodesArray));
restoreNodeState(mRoot, openNodes);
} else {
LOG.error(
"Unable to restore tree state",
"treeState=" + state,
"treeView=" + mFileTreeView);
}
}
private void restoreNodeState(@NonNull TreeNode root, Set<String> openNodes) {
final var children = root.getChildren();
for (int i = 0, childrenSize = children.size(); i < childrenSize; i++) {
final var node = children.get(i);
if (openNodes.contains(node.getPath())) {
listNode(
node,
() -> {
updateChevron(node);
expandNode(node);
restoreNodeState(node, openNodes);
});
}
}
}
public interface FileActionListener {
CodeEditorView openFile(File file);
@@ -17,7 +17,6 @@
*/
package com.itsaky.androidide.fragments.preferences;
import com.itsaky.androidide.managers.PreferenceManager;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_AUTO_SAVE;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_DRAW_HEX;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_LINE_BREAK;
@@ -44,6 +43,7 @@ import androidx.preference.SwitchPreference;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.R;
import com.itsaky.androidide.databinding.LayoutTextSizeSliderBinding;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.utils.DialogUtils;
@@ -1,280 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.handlers;
import android.view.View;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.R;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.project.AndroidProject;
import com.itsaky.androidide.project.IDEModule;
import com.itsaky.androidide.project.IDEProject;
import com.itsaky.androidide.services.builder.BuildListener;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.GradleTask;
import com.itsaky.androidide.tasks.gradle.build.ApkGeneratingTask;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.Environment;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
public class BuildServiceHandler extends IDEHandler implements BuildListener {
private IDEService service;
private boolean shouldInstallApk = false;
public BuildServiceHandler(Provider provider) {
super(provider);
}
public IDEService getService() {
return service;
}
public boolean isBuilding() {
return getService().isBuilding();
}
@Override
public void start() {
Objects.requireNonNull(this.activity());
AndroidProject project = provider.provideAndroidProject();
Objects.requireNonNull(project);
Objects.requireNonNull(project.getProjectPath());
this.service = new IDEService(new File(project.getProjectPath()));
this.service.setListener(this);
}
@Override
public void stop() {
this.service.stopAllDaemons();
this.service.exit();
}
@Override
public void onBuildModified() {
// Does nothing currently
}
@Override
public void onProjectLoaded(IDEProject project, Optional<IDEModule> appModule) {
project.iconPath = provider.provideAndroidProject().getIconPath();
activity().setIDEProject(project);
if (appModule.isPresent()) {
IDEModule app = appModule.get();
activity().setStatus(activity().getString(R.string.msg_starting_completion));
final List<String> paths =
app.dependencies.stream()
.filter(this::isClasspathValid)
.collect(Collectors.toList());
provider.provideAndroidProject().setClassPaths(paths);
File androidJar =
new File(
Environment.ANDROID_HOME,
String.format("platforms/%s/android.jar", app.compileSdkVersion));
if (androidJar.exists()) {
Environment.setBootClasspath(androidJar);
provider.provideAndroidProject().addClasspath(androidJar.getAbsolutePath());
activity().updateServices();
} else {
activity().setStatus("android.jar not found!");
}
} else {
activity().setStatus("Cannot get :app module...");
}
}
@Override
public void onStartingGradleDaemon(GradleTask task) {
activity().setStatus(activity().getString(R.string.msg_starting_daemon));
}
@Override
public void onRunTask(GradleTask task, String name) {
if (task != null && task.canOutput()) {
activity().setStatus(service.typeString(task.getTaskID()) + " " + name);
}
}
@Override
public void onBuildSuccessful(GradleTask task, String msg) {
appendOutput(task, msg);
analyzeCurrentFile();
if (task == null || activity() == null) {
return;
}
if (task.canOutput()) {
activity().setStatus(msg);
}
// If this task generates APK, ask user if he/she wants to install them
if (shouldInstallApk && task instanceof ApkGeneratingTask) {
final IDEProject project = activity().provideIDEProject();
final Optional<IDEModule> app =
project.getModuleByPath(":app"); // TODO Handle multiple application modules
if (app.isPresent()) {
final String path = app.get().projectDir;
if (path != null) {
final File buildDir = new File(path, "build");
installApks(((ApkGeneratingTask) task).getApks(buildDir));
}
}
}
activity()
.getApp()
.getPrefManager()
.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false);
activity().getBinding().buildProgressIndicator.setVisibility(View.GONE);
}
@Override
public void onBuildFailed(GradleTask task, String msg) {
appendOutput(task, msg);
analyzeCurrentFile();
if (task == null) {
return;
}
if (task.canOutput()) {
activity().setStatus(msg);
}
activity()
.getApp()
.getPrefManager()
.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false);
activity().getBinding().buildProgressIndicator.setVisibility(View.GONE);
}
@Override
public void saveFiles() {
activity().saveAll(false, false);
}
@Override
public void appendOutput(GradleTask task, CharSequence text) {
if (text == null) {
return;
}
if (task != null && !task.canOutput()) {
return;
}
activity().appendBuildOut(text.toString());
}
@Override
public void prepareBuild() {
boolean isFirstBuild =
activity()
.getApp()
.getPrefManager()
.getBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, true);
activity()
.setStatus(
activity()
.getString(
isFirstBuild
? R.string.preparing_first
: R.string.preparing));
if (isFirstBuild) {
activity().showFirstBuildNotice();
}
activity().getBinding().buildProgressIndicator.setVisibility(View.VISIBLE);
}
public void setShouldInstallApk(boolean install) {
this.shouldInstallApk = install;
}
public void assembleDebug(boolean shouldInstallApk) {
setShouldInstallApk(shouldInstallApk);
getService().assembleDebug();
}
private boolean isClasspathValid(String path) {
if (path == null || path.trim().length() <= 0 || path.trim().equals("/")) {
return false;
}
File file = new File(path);
return file.isFile()
&& file.getName().endsWith(".jar")
// Release R.jar shouldn't be included in classpath
&& !file.getAbsolutePath().endsWith("/release/R.jar");
}
private void installApks(final Set<File> apks) {
if (activity() == null || apks == null || apks.isEmpty()) {
LOG.error("Cannot install APK. Activity=" + activity(), "apks=" + apks);
return;
}
if (apks.size() == 1) {
activity().install(apks.iterator().next());
} else {
LOG.info("Multiple APKs found. Let the user select...");
final List<File> files = new ArrayList<>(apks);
final MaterialAlertDialogBuilder builder =
DialogUtils.newMaterialDialogBuilder(activity());
builder.setTitle(activity().getString(R.string.title_install_apks));
builder.setItems(
getNames(files),
(d, w) -> {
d.dismiss();
activity().install(files.get(w));
});
builder.show();
}
}
private CharSequence[] getNames(final Collection<File> apks) {
final String[] names = new String[apks.size()];
int i = 0;
for (File apk : apks) {
names[i] = apk.getName();
++i;
}
return names;
}
private void analyzeCurrentFile() {
final var editorView = activity().getCurrentEditor();
if (editorView != null) {
final var editor = editorView.getEditor();
if (editor != null) {
editor.analyze();
}
}
}
}
@@ -0,0 +1,169 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.handlers
import android.content.DialogInterface
import android.text.Layout.Alignment.ALIGN_CENTER
import android.text.Layout.Alignment.ALIGN_NORMAL
import android.text.SpannableStringBuilder
import android.text.style.AlignmentSpan
import android.view.View
import android.webkit.URLUtil
import androidx.core.view.GravityCompat
import com.itsaky.androidide.EditorActivity
import com.itsaky.androidide.R.string
import com.itsaky.androidide.managers.PreferenceManager
import com.itsaky.androidide.services.GradleBuildService
import com.itsaky.androidide.tooling.events.ProgressEvent
import com.itsaky.androidide.tooling.events.StatusEvent
import com.itsaky.androidide.tooling.events.download.FileDownloadFinishEvent
import com.itsaky.androidide.tooling.events.download.FileDownloadOperationDescriptor
import com.itsaky.androidide.tooling.events.task.TaskProgressEvent
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.StudioUtils
import java.io.File
import java.lang.ref.WeakReference
/**
* Handles events received from [GradleBuildService] updates [EditorActivity].
* @author Akash Yadav
*/
class EditorEventListener : GradleBuildService.EventListener {
private var activityReference: WeakReference<EditorActivity?> = WeakReference(null)
private val log = ILogger.newInstance(javaClass.simpleName)
fun setActivity(activity: EditorActivity) {
this.activityReference = WeakReference(activity)
}
override fun prepareBuild() {
val isFirstBuild =
activity()
.app.prefManager.getBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, true)
activity()
.setStatus(
activity()
.getString(if (isFirstBuild) string.preparing_first else string.preparing))
if (isFirstBuild) {
activity().showFirstBuildNotice()
}
activity().binding.buildProgressIndicator.visibility = View.VISIBLE
}
override fun onBuildSuccessful(tasks: MutableList<String>) {
analyzeCurrentFile()
appendOutputSeparator()
activity().app.prefManager.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false)
activity().binding.buildProgressIndicator.visibility = View.GONE
}
override fun onProgressEvent(event: ProgressEvent) {
if (event is FileDownloadFinishEvent) {
activity().setStatus("")
} else if (event is TaskProgressEvent) {
activity()
.setStatus(activity().getString(string.msg_running_task, event.descriptor.taskPath))
} else if (event is StatusEvent && event.descriptor is FileDownloadOperationDescriptor) {
val total = StudioUtils.bytesToSizeString(event.total)
val progress = StudioUtils.bytesToSizeString(event.progress)
val fileName =
URLUtil.guessFileName(
(event.descriptor as FileDownloadOperationDescriptor).uri.path, null, null)
val status = SpannableStringBuilder()
status.append(
"[$progress/$total]",
AlignmentSpan.Standard(ALIGN_NORMAL),
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE)
status.append(
" Download $fileName",
AlignmentSpan.Standard(ALIGN_CENTER),
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE)
activity().setStatus(status, GravityCompat.START)
}
}
private fun appendOutputSeparator() {
activity().appendBuildOut("\n\n")
}
override fun onBuildFailed(tasks: MutableList<String>) {
analyzeCurrentFile()
appendOutputSeparator()
activity().app.prefManager.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false)
activity().binding.buildProgressIndicator.visibility = View.GONE
}
override fun onOutput(line: String?) {
activity().appendBuildOut(line)
// TODO This can be handled better when ProgressEvents are received from Tooling API server
if (line!!.contains("BUILD SUCCESSFUL") || line.contains("BUILD FAILED")) {
activity().setStatus(line)
}
}
private fun analyzeCurrentFile() {
val editorView = activity().currentEditor
if (editorView != null) {
val editor = editorView.editor
editor?.analyze()
}
}
fun activity(): EditorActivity {
return activityReference.get()
?: throw IllegalStateException("Activity reference has been destroyed!")
}
private fun installApks(apks: Set<File>?) {
if (apks == null || apks.isEmpty()) {
IDEHandler.LOG.error("Cannot install APKs: $apks")
return
}
if (apks.size == 1) {
activity().install(apks.iterator().next())
} else {
IDEHandler.LOG.info("Multiple APKs found. Let the user select...")
val files: List<File> = ArrayList(apks)
val builder = DialogUtils.newMaterialDialogBuilder(activity())
builder.setTitle(activity().getString(string.title_install_apks))
builder.setItems(getNames(files)) { d: DialogInterface, w: Int ->
d.dismiss()
activity().install(files[w])
}
builder.show()
}
}
private fun getNames(apks: Collection<File>): Array<String?> {
val names = arrayOfNulls<String>(apks.size)
for ((i, apk) in apks.withIndex()) {
names[i] = apk.name
}
return names
}
}
@@ -29,6 +29,7 @@ import com.itsaky.androidide.databinding.LayoutCreateFileJavaBinding;
import com.itsaky.androidide.databinding.LayoutDialogTextInputBinding;
import com.itsaky.androidide.fragments.sheets.OptionsListFragment;
import com.itsaky.androidide.models.SheetOption;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.ProjectWriter;
@@ -93,34 +94,27 @@ public class FileOptionsHandler extends IDEHandler
if (forceUnknownType) {
createNewEmptyFile(f);
} else {
final var projectDir = ProjectManager.INSTANCE.getProjectDirPath ();
Objects.requireNonNull(projectDir);
final boolean isJava =
Pattern.compile(
Pattern.quote(androidProject().getProjectPath())
+ JAVA_PATH_REGEX)
Pattern.compile(Pattern.quote(projectDir) + JAVA_PATH_REGEX)
.matcher(f.getAbsolutePath())
.find();
final boolean isRes =
Pattern.compile(
Pattern.quote(androidProject().getProjectPath())
+ RES_PATH_REGEX)
Pattern.compile(Pattern.quote(projectDir) + RES_PATH_REGEX)
.matcher(f.getAbsolutePath())
.find();
final boolean isLayoutRes =
Pattern.compile(
Pattern.quote(androidProject().getProjectPath())
+ LAYOUT_RES_PATH_REGEX)
Pattern.compile(Pattern.quote(projectDir) + LAYOUT_RES_PATH_REGEX)
.matcher(f.getAbsolutePath())
.find();
final boolean isMenuRes =
Pattern.compile(
Pattern.quote(androidProject().getProjectPath())
+ MENU_RES_PATH_REGEX)
Pattern.compile(Pattern.quote(projectDir) + MENU_RES_PATH_REGEX)
.matcher(f.getAbsolutePath())
.find();
final boolean isDrawableRes =
Pattern.compile(
Pattern.quote(androidProject().getProjectPath())
+ DRAWABLE_RES_PATH_REGEX)
Pattern.compile(Pattern.quote(projectDir) + DRAWABLE_RES_PATH_REGEX)
.matcher(f.getAbsolutePath())
.find();
if (isJava) {
@@ -18,8 +18,6 @@
package com.itsaky.androidide.handlers;
import com.itsaky.androidide.EditorActivity;
import com.itsaky.androidide.project.AndroidProject;
import com.itsaky.androidide.project.IDEProject;
import com.itsaky.androidide.utils.ILogger;
/**
@@ -36,22 +34,14 @@ public abstract class IDEHandler {
this.provider = provider;
}
protected EditorActivity activity() {
return provider.provideEditorActivity();
}
protected AndroidProject androidProject() {
return provider.provideAndroidProject();
}
protected IDEProject ideProject() {
return provider.provideIDEProject();
}
public abstract void start();
public abstract void stop();
protected EditorActivity activity() {
return provider.provideEditorActivity();
}
/** An interface to communicate between a handler and its client */
public interface Provider {
@@ -61,19 +51,5 @@ public abstract class IDEHandler {
* @throws NullPointerException is this is required
*/
EditorActivity provideEditorActivity();
/**
* Called by handler to get a reference of the current {@link AndroidProject}
*
* @throws NullPointerException is this is required
*/
AndroidProject provideAndroidProject();
/**
* Called by handler to get a reference of the current {@link IDEProject}
*
* @throws NullPointerException is this is required
*/
IDEProject provideIDEProject();
}
}
@@ -18,7 +18,6 @@
package com.itsaky.androidide.language.xml;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.OPERATOR;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.TEXT_SELECTED;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.forComment;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.get;
import static io.github.rosemoe.sora.widget.schemes.EditorColorScheme.LITERAL;
@@ -25,8 +25,8 @@ import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.language.CommonCompletionProvider;
import com.itsaky.androidide.language.IDELanguage;
import com.itsaky.androidide.lexers.xml.XMLLexer;
import com.itsaky.androidide.utils.JavaCharacter;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.JavaCharacter;
import com.itsaky.androidide.views.editor.IDEEditor;
import com.itsaky.lsp.api.ILanguageServer;
@@ -38,8 +38,8 @@ import com.itsaky.androidide.models.DiagnosticGroup;
import com.itsaky.androidide.models.SearchResult;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.views.editor.CodeEditorView;
import com.itsaky.androidide.views.editor.IDEEditor;
import com.itsaky.lsp.api.ILanguageClient;
@@ -17,15 +17,26 @@
*/
package com.itsaky.androidide.models;
import static com.itsaky.androidide.utils.ILogger.newInstance;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.JSONUtility;
import com.itsaky.androidide.utils.ListingFileRedirect;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.List;
@SuppressWarnings("unused")
public class ApkMetadata {
private static final ILogger LOG = newInstance("ApkMetadata");
@SerializedName("version")
private int version;
@@ -44,12 +55,92 @@ public class ApkMetadata {
@SerializedName("elementType")
private String elementType;
public int getVersion() {
return version;
@Nullable
public static File findApkFile(@NonNull File listingFIle) {
try {
// This sometimes might be a redirect to the listing file
// So we have to handle this case too.
final var redirectedFile = ListingFileRedirect.maybeExtractRedirectedFile(listingFIle);
if (redirectedFile != null) {
listingFIle = redirectedFile;
}
final var dir = listingFIle.getParentFile();
final var metadata =
JSONUtility.gson.fromJson(new FileReader(listingFIle), ApkMetadata.class);
if (!isValid(metadata)) {
LOG.warn("Invalid APK metadata:", metadata);
return null;
}
for (Element element : metadata.getElements()) {
if (element == null || element.getOutputFile() == null) {
LOG.warn("No output file specified in APK metadata element:", element);
continue;
}
if (element.getOutputFile().endsWith(".apk")) {
final File apk = new File(dir, element.getOutputFile());
if (apk.exists() && apk.isFile()) {
LOG.info("Found apk in metadata:", apk);
return apk;
}
}
}
return null;
} catch (FileNotFoundException e) {
LOG.error("Metadata file not found...", e);
return null;
}
}
public void setVersion(int version) {
this.version = version;
private static boolean isValid(ApkMetadata metadata) {
// Null checks
if (metadata == null
|| metadata.getArtifactType() == null
|| metadata.getArtifactType().getType() == null
|| metadata.getElements() == null) {
LOG.warn("APK metadata null check failed. Metadata:", metadata);
return false;
}
final ArtifactType type = metadata.getArtifactType();
final List<Element> elements = metadata.getElements();
if (!type.getType().equals(ArtifactType.TYPE_APK)) {
LOG.warn("Artifact is not of type APK. Metadata:", metadata);
return false;
}
if (elements.isEmpty()) {
LOG.warn("No output elements found for metadata:", metadata);
return false;
}
boolean atLeastOneApk = false;
for (Element element : elements) {
if (element == null
|| element.getOutputFile() == null
|| !element.getOutputFile().endsWith(".apk")) {
LOG.warn("Skipping output element because file is not APK:", element);
continue;
}
if (element.getOutputFile().endsWith(".apk")) {
atLeastOneApk = true;
break;
}
}
LOG.debug("Output metadata validation succeeded");
return atLeastOneApk;
}
public List<Element> getElements() {
return elements;
}
public ArtifactType getArtifactType() {
@@ -60,6 +151,18 @@ public class ApkMetadata {
this.artifactType = artifactType;
}
public void setElements(List<Element> elements) {
this.elements = elements;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
public String getApplicationId() {
return applicationId;
}
@@ -76,14 +179,6 @@ public class ApkMetadata {
this.variantName = variantName;
}
public List<Element> getElements() {
return elements;
}
public void setElements(List<Element> elements) {
this.elements = elements;
}
public String getElementType() {
return elementType;
}
@@ -1,225 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.project;
import android.os.Parcel;
import android.os.Parcelable;
import com.itsaky.androidide.app.StudioApp;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class AndroidProject implements Parcelable {
private String appName;
private String packageName;
private String iconPath;
private String projectPath;
private String mainModule;
private String mainSourcePath;
private List<String> modulePaths = new ArrayList<>();
private List<String> sourcePaths = new ArrayList<>();
private List<String> classPaths = new ArrayList<>();
public AndroidProject() {
addSource(StudioApp.getInstance().getLogSenderDir().getAbsolutePath());
}
protected AndroidProject(Parcel in) {
appName = in.readString();
packageName = in.readString();
iconPath = in.readString();
projectPath = in.readString();
mainModule = in.readString();
mainSourcePath = in.readString();
in.readList(modulePaths, String.class.getClassLoader());
in.readList(sourcePaths, String.class.getClassLoader());
in.readList(classPaths, String.class.getClassLoader());
}
public File getBuildDir() {
return new File(getMainModulePath(), "build");
}
public AndroidProject setMainModule(String mainModule) {
this.mainModule = mainModule;
return this;
}
public String getMainModule() {
return mainModule;
}
public String getMainModulePath() {
return new File(getProjectPath(), getMainModule()).getAbsolutePath();
}
public AndroidProject setMainSourcePath(String mainModulePath) {
this.mainSourcePath = mainModulePath;
return this;
}
public String getMainSourcePath() {
return mainSourcePath;
}
public AndroidProject setModulePaths(List<String> modulePaths) {
this.modulePaths = modulePaths;
return this;
}
public List<String> getModulePaths() {
if (modulePaths == null) modulePaths = new ArrayList<>();
return modulePaths;
}
public AndroidProject addModule(String path) {
if (!getModulePaths().contains(path)) getModulePaths().add(path);
return this;
}
public AndroidProject setSourcePaths(List<String> sourcePaths) {
this.sourcePaths = sourcePaths;
return this;
}
public List<String> getSourcePaths() {
if (sourcePaths == null) sourcePaths = new ArrayList<>();
return sourcePaths;
}
public AndroidProject addSource(String path) {
if (!getSourcePaths().contains(path)) getSourcePaths().add(path);
return this;
}
public AndroidProject setClassPaths(List<String> classPaths) {
this.classPaths = classPaths;
return this;
}
public List<String> getClassPaths() {
if (classPaths == null) classPaths = new ArrayList<>();
return classPaths;
}
public AndroidProject addClasspath(String path) {
if (!getClassPaths().contains(path)) getClassPaths().add(path);
return this;
}
public AndroidProject setAppName(String appName) {
this.appName = appName;
return this;
}
public String getAppName() {
return appName;
}
public AndroidProject setPackageName(String packageName) {
this.packageName = packageName;
return this;
}
public String getPackageName() {
return packageName;
}
public AndroidProject setIconPath(String iconPath) {
this.iconPath = iconPath;
return this;
}
public String getIconPath() {
return iconPath;
}
public AndroidProject setProjectPath(String projectPath) {
this.projectPath = projectPath;
return this;
}
public String getProjectPath() {
return projectPath;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel p1, int p2) {
p1.writeString(appName);
p1.writeString(packageName);
p1.writeString(iconPath);
p1.writeString(projectPath);
p1.writeString(mainModule);
p1.writeString(mainSourcePath);
p1.writeList(modulePaths);
p1.writeList(sourcePaths);
p1.writeList(classPaths);
}
@Override
public String toString() {
return "["
+ "\nappName="
+ getAppName()
+ "\npackageName="
+ getPackageName()
+ "\niconPath="
+ getIconPath()
+ "\nprojectPath="
+ getProjectPath()
+ "\nmainModule="
+ getMainModule()
+ "\nmainModulePath="
+ getMainModulePath()
+ "\nmainModuleSourcePath="
+ getMainSourcePath()
+ "\nmodules="
+ getModulePaths()
+ "\nsources="
+ getSourcePaths()
+ "\nclasspaths="
+ getClassPaths()
+ "\n]";
}
public static final Creator<AndroidProject> CREATOR =
new Creator<AndroidProject>() {
@Override
public AndroidProject createFromParcel(Parcel p1) {
return new AndroidProject(p1);
}
@Override
public AndroidProject[] newArray(int p1) {
return new AndroidProject[p1];
}
};
}
@@ -1,68 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.project;
import com.google.gson.annotations.SerializedName;
/**
* Represents a module (subproject) of a root project.
*
* <p>In Android projects, a module can either be an application project, or a library project.
*/
public class IDEModule extends IDEProject {
/** Build tools version of this project */
@SerializedName("buildToolsVersion")
public String buildToolsVersion;
/**
* Compile SDK version of this project
*
* <p>Representation: 'android-31', 'android-30', 'android-29', etc
*/
@SerializedName("compileSdkVersion")
public String compileSdkVersion;
/** Minimum SDK version of this project */
@SerializedName("minSdk")
public SDK minSdk;
/** Target SDK version of this project */
@SerializedName("targetSdk")
public SDK targetSdk;
/** Version code of this project */
@SerializedName("versionCode")
public int versionCode = 0;
/** Version name of this project */
@SerializedName("versionName")
public String versionName = "Not defined";
/** Is this project an Android library project? */
@SerializedName("isLibrary")
public boolean isLibrary = true;
/** Does this module has viewBinding enabled? */
@SerializedName("viewBindingEnabled")
public boolean viewBindingEnabled;
/** Library projects don't have applicationId, application projects do. */
@SerializedName("applicationId")
public String applicationId;
}
@@ -1,96 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.project;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/** A model that contains data of a project currently opened in AndroidIDE */
public class IDEProject {
/** Name of this project */
@SerializedName("name")
public String name = "";
/** Name of this project which should be displayed to user */
@SerializedName("displayName")
public String displayName = "";
/** Description of this project. Maybe null. */
@SerializedName("description")
public String description = "";
/**
* Path of this project. This is NOT the directory path of this project.
*
* <p>For example, ':app or ':module:subModule'
*/
@SerializedName("path")
public String path = "";
/** Path of the directory containing this project */
@SerializedName("projectDir")
public String projectDir;
/** File path of the application's icon We get this manually by parsing AndroidManifest.xml */
public String iconPath;
/** Subprojects of this project */
@SerializedName("modules")
public List<IDEModule> modules = new ArrayList<>();
/** Tasks included in this project */
@SerializedName("tasks")
public List<IDETask> tasks = new ArrayList<>();
/** Path of jar file of dependencies of this project */
@SerializedName("dependencies")
public List<String> dependencies = new ArrayList<>();
/**
* Get the module by its path
*
* @param path Path of the module
* @return An Optional containing the IDEModule or Optional.empty()
*/
public Optional<IDEModule> getModuleByPath(String path) {
if (modules != null && modules.size() > 0) {
for (IDEModule module : modules) {
if (module == null) continue;
if (module.path.trim().equals(path.trim())) return Optional.of(module);
}
}
return Optional.empty();
}
/** Stores information about a SDK. Like, • Codename • API string • API level */
public static class SDK {
@SerializedName("codename")
public String codename;
@SerializedName("apiString")
public String apiString;
@SerializedName("apiLevel")
public int apiLevel;
}
}
@@ -1,44 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.project;
import com.google.gson.annotations.SerializedName;
/** Represents a Gradle task */
public class IDETask {
/** Name of this task */
@SerializedName("name")
public String name;
/** Description of this task */
@SerializedName("description")
public String description;
/** Name of the group that this task belongs to */
@SerializedName("group")
public String group;
/**
* Path of this task
*
* <p>For example, ':app:assembleDebug', ':app:build'
*/
@SerializedName("path")
public String path;
}
@@ -0,0 +1,248 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.projects
import com.android.builder.model.v2.ide.LibraryType.ANDROID_LIBRARY
import com.android.builder.model.v2.ide.LibraryType.JAVA_LIBRARY
import com.android.builder.model.v2.ide.LibraryType.PROJECT
import com.itsaky.androidide.app.StudioApp
import com.itsaky.androidide.builder.BuildService
import com.itsaky.androidide.tooling.api.model.IdeAndroidModule
import com.itsaky.androidide.tooling.api.model.IdeGradleProject
import com.itsaky.androidide.tooling.api.model.IdeJavaModule
import com.itsaky.androidide.tooling.api.model.IdeModule
import com.itsaky.lsp.java.models.JavaServerConfiguration
import java.io.File
import java.nio.file.Path
/**
* Manages projects in AndroidIDE.
*
* @author Akash Yadav
*/
object ProjectManager {
private val log = com.itsaky.androidide.utils.ILogger.newInstance(javaClass.simpleName)
var rootProject: IdeGradleProject? = null
var projectPath: String? = null
fun getProjectDir(): File? =
if (getProjectDirPath() == null) null else File(getProjectDirPath()!!)
fun getProjectDirPath(): String? =
if (rootProject == null) projectPath else rootProject!!.projectDir!!.absolutePath
fun generateSources(builder: BuildService?) {
if (builder == null) {
log.warn("Cannot generate sources. BuildService is null.")
return
}
val app =
getApplicationModule()
?: kotlin.run {
log.error("Cannot generate resources...")
return
}
val debug = app.variants.firstOrNull { it.name == "debug" }
if (debug == null) {
log.warn("No debug variant found in application project ${app.name}")
return
}
val mainArtifact = app.variants.first { it.name == "debug" }.mainArtifact
val genResourcesTask = mainArtifact.resGenTaskName
val genSourcesTask = mainArtifact.sourceGenTaskName
builder
.executeProjectTasks(
app.projectPath!!,
genResourcesTask ?: "",
genSourcesTask,
// If view binding is enabled, generate the view binding classes too
if (app.viewBindingOptions != null && app.viewBindingOptions!!.isEnabled)
"dataBindingGenBaseClassesDebug"
else "")
.whenComplete { result, err ->
if (!result.isSuccessful || err != null) {
log.warn(
"Execution for tasks '$genResourcesTask' and '$genSourcesTask' failed.",
err ?: "")
return@whenComplete
}
notifyProjectUpdate()
}
}
fun getApplicationModule(): IdeAndroidModule? {
if (rootProject == null) {
log.error(
"No root project instance is set. Is the project initialization process finished?")
return null
}
val app = rootProject!!.findFirstAndroidModule()
if (app == null) {
log.error("No application module found in root project.")
return null
}
return app
}
fun getApplicationResDirectories(): List<File>? {
return collectResDirectories(getApplicationModule() ?: return null)
}
fun collectResDirectories(android: IdeAndroidModule): List<File> {
val main = android.mainSourceSet
if (main == null) {
log.error("No main source set found in application module: ${android.name}")
return emptyList()
}
val dirs = mutableListOf<File>()
if (main.sourceProvider.resDirectories != null) {
dirs.addAll(main.sourceProvider.resDirectories!!)
}
val dependencies =
collectProjectDependencies(rootProject!!, android)
.filterIsInstance(IdeAndroidModule::class.java)
for (dependency in dependencies) {
dirs.addAll(collectResDirectories(dependency))
}
return dirs
}
fun notifyProjectUpdate() {
val server = StudioApp.getInstance().javaLanguageServer
val sourceDirs = collectApplicationSourceDirs()
val classPaths = collectApplicationClassPaths()
val configuration = JavaServerConfiguration(classPaths, sourceDirs)
server.configurationChanged(configuration)
}
private fun collectApplicationClassPaths(): Set<Path> {
val app = getApplicationModule() ?: return emptySet()
val libraries = app.variantDependencies["debug"]!!.libraries
val paths = mutableSetOf<Path>()
for (value in libraries.values) {
if (value.type == ANDROID_LIBRARY) {
paths.addAll(value.androidLibraryData!!.compileJarFiles.map { it.toPath() })
} else if (value.type == JAVA_LIBRARY) {
paths.add(value.artifact!!.toPath())
} else {
val project =
rootProject!!.findByPath(value.projectInfo!!.projectPath)!! as IdeModule
paths.add(project.getGeneratedJar("debug").toPath())
if (project is IdeAndroidModule && project.projectPath != app.projectPath) {
// R.jar and maybe other JARs
paths.addAll(
project.variants
.first { it.name == "debug" }
.mainArtifact.classesFolders
.filter { it.name.endsWith(".jar") }
.map { it.toPath() })
}
}
}
return paths
}
private fun collectApplicationSourceDirs(): Set<Path> {
val app = getApplicationModule() ?: return emptySet()
val sources = app.mainSourceSet ?: return emptySet()
val javaDirs = sources.sourceProvider.javaDirectories
val sourcePaths = javaDirs.toMutableSet()
val projectSources = collectSourceDirs(collectProjectDependencies(rootProject!!, app))
sourcePaths.addAll(projectSources)
return sourcePaths.map { it.toPath() }.toSet()
}
fun collectProjectDependencies(
project: IdeGradleProject,
app: IdeAndroidModule
): List<IdeModule> {
return app.variantDependencies["debug"]!!
.libraries.values
.filter { it.type == PROJECT }
.mapNotNull { project.findByPath(it.projectInfo!!.projectPath) }
.filterIsInstance(IdeModule::class.java)
}
fun collectSourceDirs(projects: List<IdeModule>): Set<File> {
val sources = mutableSetOf<File>()
for (project in projects) {
if (project is IdeJavaModule) {
sources.addAll(collectSources(project))
} else if (project is IdeAndroidModule) {
sources.addAll(collectSources(project))
}
}
return sources
}
fun collectSourceDirs(vararg projects: IdeModule): List<File> {
val sources = mutableListOf<File>()
for (project in projects) {
if (project is IdeJavaModule) {
sources.addAll(collectSources(project))
} else if (project is IdeAndroidModule) {
sources.addAll(collectSources(project))
}
}
return sources
}
fun collectSources(java: IdeJavaModule): List<File> {
val sources = mutableListOf<File>()
for (root in java.contentRoots) {
sources.addAll(root.sourceDirectories.map { it.directory })
}
return sources
}
fun collectSources(android: IdeAndroidModule): List<File> {
if (android.mainSourceSet == null) {
return mutableListOf()
}
// src/main/java
val sources = android.mainSourceSet!!.sourceProvider.javaDirectories.toMutableList()
// src/main/kotlin
sources.addAll(android.mainSourceSet!!.sourceProvider.kotlinDirectories)
// build/generated/**
// AIDL, ViewBinding, Renderscript, BuildConfig i.e every generated source sources
sources.addAll(
android.variants.first { it.name == "debug" }.mainArtifact.generatedSourceFolders)
return sources
}
}
@@ -15,7 +15,7 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.project;
package com.itsaky.androidide.projects;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -0,0 +1,566 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.services;
import static com.itsaky.androidide.managers.ToolsManager.getCommonAsset;
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 android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.blankj.utilcode.util.ResourceUtils;
import com.blankj.utilcode.util.ThreadUtils;
import com.blankj.utilcode.util.ZipUtils;
import com.itsaky.androidide.BuildConfig;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.BaseApplication;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.builder.BuildService;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.models.LogLine;
import com.itsaky.androidide.projects.ProjectManager;
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.TaskExecutionMessage;
import com.itsaky.androidide.tooling.api.messages.result.BuildCancellationRequestResult;
import com.itsaky.androidide.tooling.api.messages.result.BuildResult;
import com.itsaky.androidide.tooling.api.messages.result.GradleWrapperCheckResult;
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.tooling.events.ProgressEvent;
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.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
/**
* 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 static final ILogger SERVER_System_err = newInstance("ToolingApiErrorStream");
private final ILogger SERVER_LOGGER = newInstance("ToolingApiServer");
private final IBinder mBinder = new GradleServiceBinder();
private boolean isToolingServerStarted = false;
private boolean isBuildInProgress = false;
private Thread toolingServerThread;
private NotificationManager notificationManager;
private IToolingApiServer server;
private EventListener eventListener;
@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 launch =
getPackageManager().getLaunchIntentForPackage(BuildConfig.APPLICATION_ID);
final var intent =
PendingIntent.getActivity(this, 0, launch, PendingIntent.FLAG_UPDATE_CURRENT);
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 (server != null) {
server.cancelCurrentBuild();
server.shutdown();
}
if (toolingServerThread != null) {
toolingServerThread.interrupt();
}
isToolingServerStarted = false;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void logMessage(@NonNull LogLine line) {
SERVER_LOGGER.log(ILogger.priority(line.priorityChar), line.formattedTagAndMessage());
}
@Override
public void logOutput(@NonNull String line) {
if (eventListener != null) {
eventListener.onOutput(line);
}
}
@Override
public void prepareBuild() {
if (eventListener != null) {
eventListener.prepareBuild();
}
}
@Override
public void onBuildSuccessful(@NonNull BuildResult result) {
if (eventListener != null) {
eventListener.onBuildSuccessful(result.getTasks());
}
}
@Override
public void onBuildFailed(@NonNull BuildResult result) {
if (eventListener != null) {
eventListener.onBuildFailed(result.getTasks());
}
}
@Override
public void onProgressEvent(@NonNull ProgressEvent event) {
if (eventListener != null) {
eventListener.onProgressEvent(event);
}
}
@NonNull
@Override
public CompletableFuture<List<String>> getBuildArguments() {
final PreferenceManager prefs = StudioApp.getInstance().getPrefManager();
final var extraArgs = new ArrayList<String>();
extraArgs.add("--init-script");
extraArgs.add(Environment.INIT_SCRIPT.getAbsolutePath());
// Override AAPT2 binary
// The one downloaded from Maven is not built for Android
extraArgs.add("-Pandroid.aapt2FromMavenOverride=" + Environment.AAPT2.getAbsolutePath());
if (prefs.isStackTraceEnabled()) {
extraArgs.add("--stacktrace");
}
if (prefs.isGradleInfoEnabled()) {
extraArgs.add("--info");
}
if (prefs.isGradleDebugEnabled()) {
extraArgs.add("--debug");
}
if (prefs.isGradleScanEnabled()) {
extraArgs.add("--scan");
}
if (prefs.isGradleWarningEnabled()) {
extraArgs.add("--warning-mode");
extraArgs.add("all");
}
if (prefs.isGradleBuildCacheEnabled()) {
extraArgs.add("--build-cache");
}
if (prefs.isGradleOfflineModeEnabled()) {
extraArgs.add("--offline");
}
return CompletableFuture.completedFuture(extraArgs);
}
@NonNull
@Override
public CompletableFuture<GradleWrapperCheckResult> checkGradleWrapperAvailability() {
return isGradleWrapperAvailable()
? CompletableFuture.completedFuture(new GradleWrapperCheckResult(true))
: installWrapper();
}
public boolean isGradleWrapperAvailable() {
final var projectDir = ProjectManager.INSTANCE.getProjectDirPath();
if (projectDir == null || TextUtils.isEmpty(projectDir)) {
return false;
}
final var projectRoot = Objects.requireNonNull(ProjectManager.INSTANCE.getProjectDir());
if (!projectRoot.exists()) {
return false;
}
final File gradlew = new File(projectRoot, "gradlew");
final File gradleWrapperJar = new File(projectRoot, "gradle/wrapper/gradle-wrapper.jar");
final File gradleWrapperProps =
new File(projectRoot, "gradle/wrapper/gradle-wrapper.properties");
return gradlew.exists() && gradleWrapperJar.exists() && gradleWrapperProps.exists();
}
private CompletableFuture<GradleWrapperCheckResult> installWrapper() {
if (eventListener != null) {
eventListener.onOutput("-------------------- NOTE --------------------");
eventListener.onOutput(getString(R.string.msg_installing_gradlew));
eventListener.onOutput("----------------------------------------------");
}
return CompletableFuture.supplyAsync(
() -> {
boolean isAvailable = false;
final File extracted = new File(Environment.TMP_DIR, "gradle-wrapper.zip");
if (ResourceUtils.copyFileFromAssets(
getCommonAsset("gradle-wrapper.zip"), extracted.getAbsolutePath())) {
try {
final List<File> files =
ZipUtils.unzipFile(
extracted, ProjectManager.INSTANCE.getProjectDir());
if (files != null && !files.isEmpty()) {
isAvailable = true;
}
} catch (IOException e) {
LOG.error("An error occurred while extracting Gradle wrapper", e);
}
} else {
LOG.error("Unable to extract gradle-plugin.zip from IDE resources.");
}
return new GradleWrapperCheckResult(isAvailable);
});
}
@NonNull
@Override
public CompletableFuture<InitializeResult> initializeProject(@NonNull String rootDir) {
checkServerStarted();
ensureTmpdir();
if (isBuildInProgress) {
logBuildInProgress();
return failedFutureForBuildInProgress();
}
isBuildInProgress = true;
return server.initialize(new InitializeProjectMessage(rootDir))
.thenApply(this::markBuildAsFinished);
}
@NonNull
@Override
public CompletableFuture<TaskExecutionResult> executeTasks(@NonNull String... tasks) {
checkServerStarted();
ensureTmpdir();
if (isBuildInProgress) {
logBuildInProgress();
return failedFutureForBuildInProgress();
}
isBuildInProgress = true;
return server.executeTasks(new TaskExecutionMessage(":", Arrays.asList(tasks)))
.thenApply(this::markBuildAsFinished);
}
@NonNull
@Override
public CompletableFuture<TaskExecutionResult> executeProjectTasks(
@NonNull String projectPath, @NonNull String... tasks) {
checkServerStarted();
ensureTmpdir();
if (isBuildInProgress) {
logBuildInProgress();
return failedFutureForBuildInProgress();
}
isBuildInProgress = true;
return server.executeTasks(new TaskExecutionMessage(projectPath, Arrays.asList(tasks)))
.thenApply(this::markBuildAsFinished);
}
@NonNull
@Override
public CompletableFuture<BuildCancellationRequestResult> cancelCurrentBuild() {
return server.cancelCurrentBuild();
}
private void checkServerStarted() {
if (!isToolingServerStarted) {
throw new IllegalStateException("Tooling API server has not been started");
}
}
private void ensureTmpdir() {
Environment.mkdirIfNotExits(Environment.TMP_DIR);
}
private void logBuildInProgress() {
LOG.warn("A build is already in progress!");
}
@NonNull
private <T> CompletableFuture<T> failedFutureForBuildInProgress() {
final var future = new CompletableFuture<T>();
future.completeExceptionally(
new CompletionException(new IllegalStateException("A build is already running!")));
return future;
}
protected <T> T markBuildAsFinished(T t) {
isBuildInProgress = false;
return t;
}
public boolean isBuildInProgress() {
return isBuildInProgress;
}
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();
}
public GradleBuildService setEventListener(EventListener eventListener) {
this.eventListener = wrap(eventListener);
return this;
}
private EventListener wrap(EventListener listener) {
if (listener == null) {
return null;
}
return new EventListener() {
@Override
public void prepareBuild() {
ThreadUtils.runOnUiThread(listener::prepareBuild);
}
@Override
public void onBuildSuccessful(@NonNull List<String> tasks) {
ThreadUtils.runOnUiThread(() -> listener.onBuildSuccessful(tasks));
}
@Override
public void onProgressEvent(@NonNull ProgressEvent event) {
ThreadUtils.runOnUiThread(() -> listener.onProgressEvent(event));
}
@Override
public void onBuildFailed(@NonNull List<String> tasks) {
ThreadUtils.runOnUiThread(() -> listener.onBuildFailed(tasks));
}
@Override
public void onOutput(String line) {
ThreadUtils.runOnUiThread(() -> listener.onOutput(line));
}
};
}
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_System_err.error(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,
// The 'java' binary executable
Environment.JAVA.getAbsolutePath(),
// Allow reflective access to private members of classes in the following
// packages:
// - java.lang
// - java.io
// - java.util
//
// If any of the model classes in 'tooling-api-model' module send/receive
// objects from the JDK, their package name must be declared here with
// '--add-opens' to prevent InaccessibleObjectException.
// For example, some of the model classes has members of type java.io.File.
// When sending/receiving these type of objects using LSP4J, members of
// these objects are reflectively accessed by Gson. If we do no specify
// '--add-opens' for 'java.io' (for java.io.File) package, JVM will throw an
// InaccessibleObjectException.
"--add-opens",
"java.base/java.lang=ALL-UNNAMED",
"--add-opens",
"java.base/java.util=ALL-UNNAMED",
"--add-opens",
"java.base/java.io=ALL-UNNAMED",
// The JAR file to run
"-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 (Throwable err) {
if (!(err instanceof CancellationException)
&& !(err instanceof InterruptedException)) {
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();
}
/** Handles events received from a Gradle build. */
public interface EventListener {
/**
* Called just before a build is started.
*
* @see IToolingApiClient#prepareBuild()
*/
void prepareBuild();
/**
* Called when a build is successful.
*
* @param tasks The tasks that were run.
* @see IToolingApiClient#onBuildSuccessful(BuildResult)
*/
void onBuildSuccessful(@NonNull List<String> tasks);
/**
* Called when a progress event is received from the Tooling API server.
*
* @param event The event model describing the event.
*/
void onProgressEvent(@NonNull ProgressEvent event);
/**
* Called when a build fails.
*
* @param tasks The tasks that were run.
* @see IToolingApiClient#onBuildFailed(BuildResult)
*/
void onBuildFailed(@NonNull List<String> tasks);
/**
* Called when the output line is received.
*
* @param line The line of the build output.
*/
void onOutput(String line);
}
}
@@ -1,83 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.services.builder;
import com.itsaky.androidide.project.IDEModule;
import com.itsaky.androidide.project.IDEProject;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Optional;
/** A client should implement this interface to get callbacks about a build */
public interface BuildListener {
/** Called to notify the client about the modification of build/generated files */
void onBuildModified();
/**
* Called when the project has been loaded and its data has been read
*
* @param project The loaded project
* @param appModule The main application module
*/
void onProjectLoaded(IDEProject project, Optional<IDEModule> appModule);
/**
* Called when a Gradle Daemon is being started
*
* @param task Task that is currently running
*/
void onStartingGradleDaemon(GradleTask task);
/**
* Called when a task is started
*
* @param task Task that is currently running
* @param name Name of this task that should be shown to user
*/
void onRunTask(GradleTask task, String name);
/**
* Notifys about a successful build
*
* @param task Task that succeeded
* @param msg Successful message
*/
void onBuildSuccessful(GradleTask task, String msg);
/**
* Notifys about a failed build
*
* @param task Task that failed
* @param msg Failure message
*/
void onBuildFailed(GradleTask task, String msg);
/** Called to notify client to save any opened files */
void saveFiles();
/**
* Notifys the client to append the build output that should be shown to user
*
* @param task Task that is currently running
* @param text Output to append
*/
void appendOutput(GradleTask task, CharSequence text);
/** Called to notify the client to prepare for a build */
void prepareBuild();
}
@@ -1,493 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.services.builder;
import static com.itsaky.androidide.managers.ToolsManager.getCommonAsset;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.ResourceUtils;
import com.blankj.utilcode.util.ZipUtils;
import com.google.gson.Gson;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.project.IDEModule;
import com.itsaky.androidide.project.IDEProject;
import com.itsaky.androidide.shell.IProcessExecutor;
import com.itsaky.androidide.shell.IProcessExitListener;
import com.itsaky.androidide.shell.ProcessExecutorFactory;
import com.itsaky.androidide.shell.ProcessStreamsHolder;
import com.itsaky.androidide.tasks.GradleTask;
import com.itsaky.androidide.tasks.gradle.BaseGradleTasks;
import com.itsaky.androidide.tasks.gradle.resources.UpdateResourceClassesTask;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.InputStreamLineReader;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.toaster.Toaster;
import org.jetbrains.annotations.Contract;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class IDEService {
public static final int TASK_SHOW_DEPENDENCIES = GradleTask.TASK_SHOW_DEPENDENCIES;
public static final int TASK_ASSEMBLE_DEBUG = GradleTask.TASK_ASSEMBLE_DEBUG;
public static final int TASK_ASSEMBLE_RELEASE = GradleTask.TASK_ASSEMBLE_RELEASE;
public static final int TASK_BUILD = GradleTask.TASK_BUILD;
public static final int TASK_BUNDLE = GradleTask.TASK_BUNDLE;
public static final int TASK_CLEAN = GradleTask.TASK_CLEAN;
public static final int TASK_CLEAN_BUILD = GradleTask.TASK_CLEAN_BUILD;
public static final int TASK_DEX = GradleTask.TASK_DEX;
public static final int TASK_LINT = GradleTask.TASK_LINT;
public static final int TASK_LINT_DEBUG = GradleTask.TASK_LINT_DEBUG;
public static final int TASK_LINT_RELEASE = GradleTask.TASK_LINT_RELEASE;
public static final ILogger LOG = ILogger.newInstance("IDEService");
private final File projectRoot;
private final IProcessExecutor processExecutor;
private BuildListener listener;
private GradleTask currentTask;
private IDEProject mIDEProject;
private IDEModule mAppModule;
private final InputStreamLineReader.OnReadListener mOutputReadListener = this::onBuildOutput;
private boolean isBuilding = false;
private final IProcessExitListener mProcessExitListener =
new IProcessExitListener() {
@Override
public void onExit(int code) {
isBuilding = false;
if (listener != null) {
final String msg = getString(R.string.msg_gradle_terminated, code);
if (code == 0) {
onBuildSuccessful(currentTask, msg);
} else {
onBuildFailed(currentTask, msg);
}
}
}
};
public IDEService(File projectRoot) {
this.projectRoot = projectRoot;
this.processExecutor = ProcessExecutorFactory.commonExecutor();
}
public IDEService setListener(final BuildListener listener) {
this.listener = listener == null ? null : new MainThreadBuildListener(listener);
return this;
}
protected void onBuildOutput(String line) {
if (listener == null || line == null) return;
line = line.trim();
String PROJECT_INITIALIZED = ">>> PROJECT INITIALIZED <<<";
if (line.contains(PROJECT_INITIALIZED)) {
readIdeProject();
return;
}
String text = line;
final var app = StudioApp.getInstance();
// Try to shorten common file paths
if (line.contains(app.getRootDir().getAbsolutePath())) {
text = line.replace(app.getRootDir().getAbsolutePath(), "HOME");
}
if (line.contains(
Objects.requireNonNull(app.getRootDir().getParentFile()).getAbsolutePath())) {
text = line.replace(app.getRootDir().getParentFile().getAbsolutePath(), "ROOT");
}
listener.appendOutput(currentTask, text);
String RUN_TASK = "> Task";
String STARTING_DAEMON = "Starting a ";
if (line.startsWith(RUN_TASK)) {
listener.onRunTask(currentTask, line.trim());
} else if (line.startsWith(STARTING_DAEMON)) {
listener.onStartingGradleDaemon(currentTask);
}
}
protected void onBuildSuccessful(GradleTask task, String msg) {
if (currentTask != null && currentTask.getTaskID() != TASK_SHOW_DEPENDENCIES) {
listener.onBuildSuccessful(task, msg.trim());
}
appendOutputSeparator();
if (currentTask != null && currentTask.affectsGeneratedSources()) {
notifyExternalSourceChange();
}
}
protected void onBuildFailed(GradleTask task, @NonNull String msg) {
isBuilding = false;
listener.onBuildFailed(task, msg.trim());
appendOutputSeparator();
}
private void readIdeProject() {
// Parsing project data is meaningless if there is no one listening...
if (listener == null) {
return;
}
final BuildListener local = listener;
new Thread(
() -> {
mIDEProject =
new Gson()
.fromJson(
FileIOUtils.readFile2String(
Environment.PROJECT_DATA_FILE),
IDEProject.class);
if (mIDEProject != null) {
Optional<IDEModule> appModule = mIDEProject.getModuleByPath(":app");
appModule.ifPresent(ideModule -> mAppModule = ideModule);
local.onProjectLoaded(mIDEProject, appModule);
} else {
StudioApp.getInstance()
.toast(
com.itsaky.androidide.R.string
.msg_init_project_failed,
Toaster.Type.ERROR);
}
})
.start();
}
private void appendOutputSeparator() {
listener.appendOutput(currentTask, "\n\n");
}
public void exit() {
// Delete the tmp directory. It is not needed anymore...
FileUtils.delete(Environment.TMP_DIR);
}
@NonNull
private String currentTime() {
String pattern = "HH:mm:ss";
DateFormat df = new SimpleDateFormat(pattern, Locale.US);
Date today = Calendar.getInstance().getTime();
return df.format(today);
}
private String getString(@StringRes int id) {
return StudioApp.getInstance().getString(id);
}
private String getString(@StringRes int id, Object... format) {
return StudioApp.getInstance().getString(id, format);
}
private void notifyExternalSourceChange() {
// TODO Watch for file changes using a file observer
// instead of depending on the build
if (listener != null) {
listener.onBuildModified();
}
}
@NonNull
private String[] getArguments(List<String> tasks) {
final PreferenceManager prefs = StudioApp.getInstance().getPrefManager();
final List<String> args = new ArrayList<>();
// 'bash' shell is always preferred
if (Environment.SHELL.exists()) {
args.add(Environment.SHELL.getAbsolutePath());
} else {
args.add(Environment.BUSYBOX.getAbsolutePath());
args.add("sh");
}
args.add(new File(projectRoot, "gradlew").getAbsolutePath());
args.addAll(asAppTasks(tasks));
args.add("--init-script");
args.add(Environment.INIT_SCRIPT.getAbsolutePath());
if (prefs.isStackTraceEnabled()) {
args.add("--stacktrace");
}
if (prefs.isGradleInfoEnabled()) {
args.add("--info");
}
if (prefs.isGradleDebugEnabled()) {
args.add("--debug");
}
if (prefs.isGradleScanEnabled()) {
args.add("--scan");
}
if (prefs.isGradleWarningEnabled()) {
args.add("--warning-mode");
args.add("all");
}
if (prefs.isGradleBuildCacheEnabled()) {
args.add("--build-cache");
}
if (prefs.isGradleOfflineModeEnabled()) {
args.add("--offline");
}
return args.toArray(new String[0]);
}
private Collection<? extends String> asAppTasks(@NonNull List<String> tasks) {
return tasks.stream()
.filter(Objects::nonNull)
.map(this::asAppModuleTask)
.collect(Collectors.toList());
}
@Contract(pure = true)
private String asAppModuleTask(@NonNull String name) {
return name.startsWith(":app:") ? name : ":app:" + name;
}
public void execTask(GradleTask task) {
execTask(task, false);
}
public void execTask(GradleTask task, boolean stopIfRunning) {
if (stopIfRunning && isBuilding()) {
stopAllDaemons();
} else if (isBuilding()) {
return;
}
execTask(
task,
isGradleWrapperAvailable()
? CompletableFuture.completedFuture(Boolean.TRUE)
: installWrapper());
}
private void execTask(GradleTask task, CompletableFuture<Boolean> installing) {
final Runnable taskRunner =
() -> {
if (installing != null) {
Boolean result;
String failure = "";
try {
result = installing.get();
} catch (Throwable th) {
result = Boolean.FALSE;
failure = th.getMessage();
}
if (result.equals(Boolean.FALSE)) {
if (listener != null) {
listener.appendOutput(
task, "-------------------- ERROR --------------------");
listener.appendOutput(
task,
getString(
R.string.msg_gradlew_installation_failed, failure));
listener.appendOutput(
task, "-----------------------------------------------");
}
return;
}
}
if (listener != null) {
final ProcessStreamsHolder holder = new ProcessStreamsHolder();
listener.saveFiles();
currentTask = task;
listener.appendOutput(
task,
getString(R.string.msg_task_begin, currentTime(), task.getName()));
try {
Environment.mkdirIfNotExits(Environment.TMP_DIR);
processExecutor.execAsync(
holder,
mProcessExitListener,
projectRoot.getAbsolutePath(),
true,
getArguments(task.getTasks()));
isBuilding = true;
startReading(holder.in);
listener.prepareBuild();
} catch (Throwable e) {
listener.onBuildFailed(task, e.getMessage());
}
}
};
final Thread thread = new Thread(taskRunner, "GradleTaskExecutor");
thread.setDaemon(true);
thread.start();
}
private void startReading(InputStream in) {
final Thread reader =
new Thread(
new InputStreamLineReader(in, mOutputReadListener),
"GradleProcessOutputReader");
reader.setDaemon(true);
reader.start();
}
private CompletableFuture<Boolean> installWrapper() {
if (listener != null) {
listener.appendOutput(null, "-------------------- NOTE --------------------");
listener.appendOutput(null, getString(R.string.msg_installing_gradlew));
listener.appendOutput(null, "----------------------------------------------");
}
return CompletableFuture.supplyAsync(
() -> {
final File extracted = new File(Environment.TMP_DIR, "gradle-wrapper.zip");
if (ResourceUtils.copyFileFromAssets(
getCommonAsset("gradle-wrapper.zip"), extracted.getAbsolutePath())) {
try {
final List<File> files = ZipUtils.unzipFile(extracted, projectRoot);
if (files != null && !files.isEmpty()) {
return Boolean.TRUE;
}
} catch (IOException e) {
return Boolean.FALSE;
}
}
return Boolean.FALSE;
});
}
public boolean isGradleWrapperAvailable() {
if (projectRoot == null || !projectRoot.exists()) {
return false;
}
final File gradlew = new File(projectRoot, "gradlew");
final File gradleWrapperJar = new File(projectRoot, "gradle/wrapper/gradle-wrapper.jar");
final File gradleWrapperProps =
new File(projectRoot, "gradle/wrapper/gradle-wrapper.properties");
return gradlew.exists() && gradleWrapperJar.exists() && gradleWrapperProps.exists();
}
public void assembleDebug() {
execTask(BaseGradleTasks.ASSEMBLE_DEBUG);
}
public void assembleRelease() {
execTask(BaseGradleTasks.ASSEMBLE_RELEASE);
}
public void build() {
execTask(BaseGradleTasks.BUILD);
}
public void bundle() {
execTask(BaseGradleTasks.BUNDLE);
}
public void clean() {
execTask(BaseGradleTasks.CLEAN);
}
public void cleanAndRebuild() {
execTask(BaseGradleTasks.CLEAN_BUILD);
}
public void stopAllDaemons() {
StudioApp.getInstance()
.newShell(null)
.bgAppend(
String.format(
Locale.US,
"cd '%s' && sh gradlew --stop",
projectRoot.getAbsolutePath()));
if (isBuilding()) {
if (listener != null)
listener.onBuildFailed(currentTask, getString(R.string.msg_daemons_stopped));
isBuilding = false;
}
}
public void lint() {
execTask(BaseGradleTasks.LINT);
}
public void lintDebug() {
execTask(BaseGradleTasks.LINT_DEBUG);
}
public void lintRelease() {
execTask(BaseGradleTasks.LINT_RELEASE);
}
public void updateResourceClasses() {
execTask(
new UpdateResourceClassesTask(mAppModule != null && mAppModule.viewBindingEnabled));
}
public boolean isBuilding() {
return isBuilding;
}
public String typeString(int type) {
switch (type) {
case TASK_ASSEMBLE_DEBUG:
return getString(R.string.build_debug);
case TASK_ASSEMBLE_RELEASE:
return getString(R.string.build_release);
case TASK_BUILD:
return getString(R.string.build);
case TASK_CLEAN_BUILD:
return getString(R.string.clean_amp_build);
case TASK_CLEAN:
return getString(R.string.clean_project);
case TASK_BUNDLE:
return getString(R.string.create_aab);
case TASK_DEX:
return getString(R.string.compiling);
default:
return "";
}
}
}
@@ -1,83 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.services.builder;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.ThreadUtils;
import com.itsaky.androidide.project.IDEModule;
import com.itsaky.androidide.project.IDEProject;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Objects;
import java.util.Optional;
class MainThreadBuildListener implements BuildListener {
private final BuildListener listener;
public MainThreadBuildListener(@NonNull final BuildListener listener) {
Objects.requireNonNull(listener, "Build listener cannot be null");
this.listener = listener;
}
@Override
public void onBuildModified() {
ThreadUtils.runOnUiThread(listener::onBuildModified);
}
@Override
public void onProjectLoaded(IDEProject project, Optional<IDEModule> appModule) {
ThreadUtils.runOnUiThread(() -> listener.onProjectLoaded(project, appModule));
}
@Override
public void onStartingGradleDaemon(GradleTask task) {
ThreadUtils.runOnUiThread(() -> listener.onStartingGradleDaemon(task));
}
@Override
public void onRunTask(GradleTask task, String name) {
ThreadUtils.runOnUiThread(() -> listener.onRunTask(task, name));
}
@Override
public void onBuildSuccessful(GradleTask task, String msg) {
ThreadUtils.runOnUiThread(() -> listener.onBuildSuccessful(task, msg));
}
@Override
public void onBuildFailed(GradleTask task, String msg) {
ThreadUtils.runOnUiThread(() -> listener.onBuildFailed(task, msg));
}
@Override
public void saveFiles() {
ThreadUtils.runOnUiThread(listener::saveFiles);
}
@Override
public void appendOutput(GradleTask task, CharSequence text) {
ThreadUtils.runOnUiThread(() -> listener.appendOutput(task, text));
}
@Override
public void prepareBuild() {
ThreadUtils.runOnUiThread(listener::prepareBuild);
}
}
@@ -1,46 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks;
import com.itsaky.androidide.services.builder.IDEService;
public abstract class BaseGradleTask implements GradleTask {
@Override
public boolean affectsGeneratedSources() {
return false;
}
@Override
public boolean shouldSaveFiles() {
return getType() == Type.BUILD
|| getType() == Type.LINT
|| getTaskID() == IDEService.TASK_DEX;
}
@Override
public boolean equals(Object obj) {
return obj != null
&& obj instanceof GradleTask
&& getTaskID() == ((GradleTask) obj).getTaskID()
&& getType() == ((GradleTask) obj).getType();
}
}
@@ -1,68 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks;
import java.util.List;
public interface GradleTask {
public String getName();
public String getCommands();
public List<String> getTasks();
public int getTaskID();
public boolean affectsGeneratedSources();
public boolean shouldSaveFiles();
public boolean canOutput();
public Type getType();
public static enum Type {
ANDROIDIDE_PLUGIN,
BUILD,
LINT,
HELP,
OTHER
}
public static final int TASK_SHOW_DEPENDENCIES = 0;
public static final int TASK_ASSEMBLE_DEBUG = 1;
public static final int TASK_ASSEMBLE_RELEASE = 2;
public static final int TASK_BUILD = 3;
public static final int TASK_BUNDLE = 4;
public static final int TASK_CLEAN = 5;
public static final int TASK_CLEAN_BUILD = 6;
public static final int TASK_COMPILE_JAVA = 7;
public static final int TASK_DEX = 8;
public static final int TASK_LINT = 9;
public static final int TASK_LINT_DEBUG = 10;
public static final int TASK_LINT_RELEASE = 11;
public static final int TASK_TASKS = 12;
public static final int TASK_INIT_PROJECT = 13;
public static final int TASK_PROCESS_DEBUG_RESOURCES = 14;
public static final int DATABINDING_GEN_BASE_CLASSES_DEBUG = 15;
public static final int TASK_UPDATE_RESOURCE_CLASSES = 16;
}
@@ -1,49 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.gradle;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.gradle.build.AssembleDebug;
import com.itsaky.androidide.tasks.gradle.build.AssembleRelease;
import com.itsaky.androidide.tasks.gradle.build.Build;
import com.itsaky.androidide.tasks.gradle.build.Bundle;
import com.itsaky.androidide.tasks.gradle.build.Clean;
import com.itsaky.androidide.tasks.gradle.build.CleanBuild;
import com.itsaky.androidide.tasks.gradle.lint.Lint;
import com.itsaky.androidide.tasks.gradle.lint.LintDebug;
import com.itsaky.androidide.tasks.gradle.lint.LintRelease;
import com.itsaky.androidide.tasks.gradle.plugin.InitializeIDEProject;
import com.itsaky.androidide.tasks.gradle.resources.ProcessDebugResources;
public class BaseGradleTasks {
public static final BaseGradleTask BUILD = new Build(),
ASSEMBLE_DEBUG = new AssembleDebug(),
ASSEMBLE_RELEASE = new AssembleRelease(),
BUNDLE = new Bundle(),
CLEAN = new Clean(),
CLEAN_BUILD = new CleanBuild(),
LINT = new Lint(),
LINT_DEBUG = new LintDebug(),
LINT_RELEASE = new LintRelease(),
PROCESS_DEBUG_RESOURCES = new ProcessDebugResources(),
INITIALIZE_IDE_PROJECT = new InitializeIDEProject();
}
@@ -1,153 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.tasks.gradle.build;
import static com.itsaky.androidide.models.ApkMetadata.ArtifactType;
import static com.itsaky.androidide.models.ApkMetadata.Element;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.models.ApkMetadata;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.utils.JSONUtility;
import com.itsaky.androidide.utils.ILogger;
import java.io.File;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public abstract class ApkGeneratingTask extends BaseGradleTask {
private static final ILogger LOG = ILogger.newInstance("ApkGeneratingTask");
/**
* Get the directories where APKs are generated. These directories must contain an
* output-metadata.json file
*/
protected abstract Set<File> provideApkDirectories(File buildDir);
protected Set<File> directories(File buildDir) {
if (buildDir == null || !buildDir.exists() || !buildDir.isDirectory()) {
return Collections.emptySet();
}
return provideApkDirectories(buildDir);
}
/** Get generated APKs */
public Set<File> getApks(File buildDir) {
final Set<File> result = new HashSet<>();
final Set<File> dirs = directories(buildDir);
if (dirs == null || dirs.isEmpty()) {
LOG.error("No apk directories found. buildDir=" + buildDir);
return result;
}
for (File dir : dirs) {
final File jsonData = new File(dir, "output-metadata.json");
if (!jsonData.exists() || !FileUtils.isUtf8(jsonData)) {
LOG.error(
"No output-metadata.json file found in APK output directory"
+ " or the file is empty. APK directory:",
dir);
continue;
}
final String contents = FileIOUtils.readFile2String(jsonData);
try {
final ApkMetadata metadata = JSONUtility.gson.fromJson(contents, ApkMetadata.class);
if (!isValid(metadata)) {
LOG.warn("Invalid APK metadata:", metadata);
continue;
}
for (Element element : metadata.getElements()) {
if (element == null || element.getOutputFile() == null) {
LOG.warn("No output file specified in APK metadata element:", element);
continue;
}
if (element.getOutputFile().endsWith(".apk")) {
final File apk = new File(dir, element.getOutputFile());
if (apk.exists() && apk.isFile()) {
LOG.info("APK generated:", apk);
result.add(apk);
}
}
}
} catch (Throwable th) {
LOG.error(
StudioApp.getInstance()
.getString(
com.itsaky.androidide.R.string
.err_cannot_parse_apk_metadata),
th);
}
}
return result;
}
private boolean isValid(ApkMetadata metadata) {
// Null checks
if (metadata == null
|| metadata.getArtifactType() == null
|| metadata.getArtifactType().getType() == null
|| metadata.getElements() == null) {
LOG.warn("APK metadata null check failed. Metadata:", metadata);
return false;
}
final ArtifactType type = metadata.getArtifactType();
final List<Element> elements = metadata.getElements();
if (!type.getType().equals(ArtifactType.TYPE_APK)) {
LOG.warn("Artifact is not of type APK. Metadata:", metadata);
return false;
}
if (elements.isEmpty()) {
LOG.warn("No output elements found for metadata:", metadata);
return false;
}
boolean atLeastOneApk = false;
for (Element element : elements) {
if (element == null
|| element.getOutputFile() == null
|| !element.getOutputFile().endsWith(".apk")) {
LOG.warn("Skipping output element because file is not APK:", element);
continue;
}
if (element.getOutputFile().endsWith(".apk")) {
atLeastOneApk = true;
break;
}
}
LOG.debug("Output metadata validation succeeded");
return atLeastOneApk;
}
}
@@ -1,69 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.tasks.gradle.build;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import java.io.File;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class AssembleDebug extends ApkGeneratingTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.build_debug);
}
@Override
protected Set<File> provideApkDirectories(File buildDir) {
final Set<File> dirs = new HashSet<>();
dirs.add(new File(buildDir, "outputs/apk/debug"));
return dirs;
}
@Override
public String getCommands() {
return "assembleDebug";
}
@Override
public List<String> getTasks() {
return Collections.singletonList(getCommands());
}
@Override
public int getTaskID() {
return IDEService.TASK_ASSEMBLE_DEBUG;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public Type getType() {
return Type.BUILD;
}
}
@@ -1,72 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.gradle.build;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.GradleTask;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class AssembleRelease extends ApkGeneratingTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.build_release);
}
@Override
protected Set<File> provideApkDirectories(File buildDir) {
final Set<File> dirs = new HashSet<>();
dirs.add(new File(buildDir, "outputs/apk/release"));
return dirs;
}
@Override
public String getCommands() {
return "assembleRelease";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return IDEService.TASK_ASSEMBLE_RELEASE;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return GradleTask.Type.BUILD;
}
}
@@ -1,73 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.gradle.build;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.GradleTask;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Build extends ApkGeneratingTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.build);
}
@Override
protected Set<File> provideApkDirectories(File buildDir) {
final Set<File> dirs = new HashSet<>();
dirs.add(new File(buildDir, "outputs/apk/debug"));
dirs.add(new File(buildDir, "outputs/apk/release"));
return dirs;
}
@Override
public String getCommands() {
return "build";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return IDEService.TASK_BUILD;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return GradleTask.Type.BUILD;
}
}
@@ -1,63 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.gradle.build;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Arrays;
import java.util.List;
public class Bundle extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.create_aab);
}
@Override
public String getCommands() {
return "bundle";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return IDEService.TASK_BUNDLE;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return GradleTask.Type.BUILD;
}
}
@@ -1,63 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.gradle.build;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Arrays;
import java.util.List;
public class Clean extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.clean_project);
}
@Override
public String getCommands() {
return "clean";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return IDEService.TASK_CLEAN;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return GradleTask.Type.BUILD;
}
}
@@ -1,63 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.gradle.build;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Arrays;
import java.util.List;
public class CleanBuild extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.clean_amp_build);
}
@Override
public String getCommands() {
return "clean build";
}
@Override
public List<String> getTasks() {
return Arrays.asList("clean", "build");
}
@Override
public int getTaskID() {
return IDEService.TASK_CLEAN_BUILD;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return GradleTask.Type.BUILD;
}
}
@@ -1,62 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.gradle.lint;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import java.util.Arrays;
import java.util.List;
public class Lint extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.lint);
}
@Override
public String getCommands() {
return "lint";
}
@Override
public List<String> getTasks() {
return Arrays.asList("lint");
}
@Override
public int getTaskID() {
return IDEService.TASK_LINT;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public Type getType() {
return Type.LINT;
}
}
@@ -1,62 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.gradle.lint;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import java.util.Collections;
import java.util.List;
public class LintDebug extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.lint_debug);
}
@Override
public String getCommands() {
return "lintDebug";
}
@Override
public List<String> getTasks() {
return Collections.singletonList("lintDebug");
}
@Override
public int getTaskID() {
return IDEService.TASK_LINT_DEBUG;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public Type getType() {
return Type.LINT;
}
}
@@ -1,62 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.gradle.lint;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.services.builder.IDEService;
import com.itsaky.androidide.tasks.BaseGradleTask;
import java.util.Arrays;
import java.util.List;
public class LintRelease extends BaseGradleTask {
@Override
public String getName() {
return StudioApp.getInstance().getString(R.string.lint_release);
}
@Override
public String getCommands() {
return "lintRelease";
}
@Override
public List<String> getTasks() {
return Arrays.asList("lintRelease");
}
@Override
public int getTaskID() {
return IDEService.TASK_LINT_RELEASE;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public Type getType() {
return Type.LINT;
}
}
@@ -1,60 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.gradle.plugin;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Arrays;
import java.util.List;
public class InitializeIDEProject extends BaseGradleTask {
@Override
public String getName() {
return "initializeIDEProject";
}
@Override
public String getCommands() {
return "initializeIDEProject";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return TASK_INIT_PROJECT;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return Type.ANDROIDIDE_PLUGIN;
}
}
@@ -1,64 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.gradle.resources;
import com.itsaky.androidide.tasks.BaseGradleTask;
import java.util.Arrays;
import java.util.List;
public class DatabindingGenBaseClassesDebug extends BaseGradleTask {
@Override
public boolean affectsGeneratedSources() {
return true;
}
@Override
public String getName() {
return "Generate databinding classes";
}
@Override
public String getCommands() {
return "databindingGenBaseClassesDebug";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return DATABINDING_GEN_BASE_CLASSES_DEBUG;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public Type getType() {
return Type.BUILD;
}
}
@@ -1,65 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.gradle.resources;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.Arrays;
import java.util.List;
public class ProcessDebugResources extends BaseGradleTask {
@Override
public boolean affectsGeneratedSources() {
return true;
}
@Override
public String getName() {
return "processDebugResources";
}
@Override
public String getCommands() {
return "processDebugResources";
}
@Override
public List<String> getTasks() {
return Arrays.asList(getCommands());
}
@Override
public int getTaskID() {
return TASK_PROCESS_DEBUG_RESOURCES;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return Type.BUILD;
}
}
@@ -1,82 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.tasks.gradle.resources;
import com.itsaky.androidide.tasks.BaseGradleTask;
import com.itsaky.androidide.tasks.GradleTask;
import java.util.ArrayList;
import java.util.List;
public class UpdateResourceClassesTask extends BaseGradleTask {
private final boolean viewBinding;
public UpdateResourceClassesTask(boolean viewBinding) {
this.viewBinding = viewBinding;
}
@Override
public boolean affectsGeneratedSources() {
return true;
}
@Override
public String getName() {
return "Update resource classes";
}
@Override
public String getCommands() {
String command = "";
if (viewBinding) {
command = new DatabindingGenBaseClassesDebug().getCommands();
command += " ";
}
command += new ProcessDebugResources().getCommands();
return command;
}
@Override
public List<String> getTasks() {
List<String> tasks = new ArrayList<>();
if (viewBinding) {
tasks.addAll(new DatabindingGenBaseClassesDebug().getTasks());
}
tasks.addAll(new ProcessDebugResources().getTasks());
return tasks;
}
@Override
public int getTaskID() {
return TASK_UPDATE_RESOURCE_CLASSES;
}
@Override
public boolean canOutput() {
return true;
}
@Override
public GradleTask.Type getType() {
return Type.BUILD;
}
}
@@ -37,9 +37,9 @@ package com.itsaky.androidide.utils
import android.content.Context
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.actions.build.CancelBuildAction
import com.itsaky.androidide.actions.build.QuickRunAction
import com.itsaky.androidide.actions.build.RunActionMenu
import com.itsaky.androidide.actions.build.StopDaemonsAction
import com.itsaky.androidide.actions.editor.CopyAction
import com.itsaky.androidide.actions.editor.CutAction
import com.itsaky.androidide.actions.editor.ExpandSelectionAction
@@ -78,7 +78,7 @@ class EditorActivityActions {
registry.registerAction(FindActionMenu(context))
registry.registerAction(FileTreeAction(context))
registry.registerAction(DaemonStatusAction(context))
registry.registerAction(StopDaemonsAction(context))
registry.registerAction(CancelBuildAction(context))
// editor text actions
registry.registerAction(ExpandSelectionAction(context))
@@ -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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
*/
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();
}
}
@@ -0,0 +1,43 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.utils
import java.io.File
import java.io.StringReader
import java.util.*
class ListingFileRedirect {
companion object {
/** Redirect file will have this marker as the first line as comment. */
private const val REDIRECT_MARKER = "#- File Locator -"
/** Property name in a [Properties] for the metadata file location. */
private const val REDIRECT_PROPERTY_NAME = "listingFile"
@JvmStatic
fun maybeExtractRedirectedFile(redirectFile: File): File? {
val fileContent = redirectFile.readText()
return if (fileContent.startsWith(REDIRECT_MARKER)) {
val fileLocator = Properties().also { it.load(StringReader(fileContent)) }
val file = File(fileLocator.getProperty(REDIRECT_PROPERTY_NAME))
if (!file.isAbsolute) redirectFile.parentFile!!.resolve(file) else file
} else null
}
}
}
@@ -1,201 +0,0 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.utils;
import android.text.TextUtils;
import com.blankj.utilcode.util.FileIOUtils;
import com.itsaky.androidide.project.AndroidProject;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileFilter;
import java.util.regex.Pattern;
import fr.arnaudguyon.xmltojsonlib.XmlToJson;
public class ProjectFinder {
public static AndroidProject fromFolder(File file) {
if (!file.isDirectory() || file.isHidden()) return null;
File settings = new File(file, "settings.gradle");
if (!settings.exists()) return null;
String content = FileUtil.readFile(settings.getAbsolutePath());
if (TextUtils.isEmpty(content)) return null;
AndroidProject project = new AndroidProject();
for (File module : file.listFiles()) {
if (module.isDirectory() && !module.isHidden()) {
try {
proceedModule(file, module, project);
} catch (JSONException e) {
continue;
}
}
}
project.setAppName(file.getName());
project.setProjectPath(file.getAbsolutePath());
if (project.getMainModule() == null
&& project.getModulePaths().size() > 0
&& project.getModulePaths().get(0) != null) {
File f = new File(project.getModulePaths().get(0));
if (!f.exists() || !f.isDirectory()) return null;
project.setMainModule(f.getName());
project.setMainSourcePath(new File(f, "src/main/java").getAbsolutePath());
}
return project;
}
private static void proceedModule(File file, File module, AndroidProject project)
throws JSONException {
final Pattern appPattern =
Pattern.compile("apply\\s+plugin\\s*:\\s*(['\"])com.android.application(['\"])");
File projectGradle = new File(file, "build.gradle");
File moduleGradle = new File(module, "build.gradle");
File manifest = new File(module, "src/main/AndroidManifest.xml");
File res = new File(module, "src/main/res");
if (!module.exists()
|| !projectGradle.exists()
|| !moduleGradle.exists()
|| !manifest.exists()) return;
if (!module.isDirectory()
|| !moduleGradle.isFile()
|| !projectGradle.isFile()
|| !manifest.isFile()) return;
File java = new File(module, "src/main/java");
if (java.exists() && java.isDirectory())
project.addSource(java.getAbsolutePath()).addModule(module.getAbsolutePath());
File libsFolder = new File(module, "libs");
if (libsFolder.exists() && libsFolder.isDirectory()) {
File[] libs = libsFolder.listFiles();
if (libs != null && libs.length > 0) {
for (File lib : libs)
if (lib.getName().endsWith(".jar")) project.addClasspath(lib.getAbsolutePath());
}
}
project.addSource(
new File(module, "build/generated/source/buildConfig/debug").getAbsolutePath());
project.addSource(
new File(module, "build/generated/data_binding_base_class_source_out/debug/out")
.getAbsolutePath());
JSONObject obj =
new XmlToJson.Builder(FileUtil.readFile(manifest.getAbsolutePath()))
.build()
.toJson();
if (!obj.has("manifest")) return;
String iconPath = null;
if (obj.has("manifest")
&& obj.getJSONObject("manifest").has("application")
&& obj.getJSONObject("manifest").getJSONObject("application").has("android:icon")) {
String iconRes =
obj.getJSONObject("manifest")
.getJSONObject("application")
.getString("android:icon");
if (iconRes.startsWith("@drawable")) {
final String iconName = iconRes.replace("@drawable/", "");
File[] drawables = listDrawables(res);
for (File drawable : drawables) {
File[] icons = listFilesNotEndingWith(drawable, ".xml");
for (File icon : icons) {
String nameWithoutExtension =
icon.getName().substring(0, icon.getName().lastIndexOf("."));
if (nameWithoutExtension.equals(iconName)) {
iconPath = icon.getAbsolutePath();
break;
}
}
}
} else if (iconRes.startsWith("@drawable")) {
final String iconName = iconRes.replace("@drawable/", "");
File[] drawables = listMipmaps(res);
for (File drawable : drawables) {
File[] icons = listFilesNotEndingWith(drawable, ".xml");
for (File icon : icons) {
String nameWithoutExtension =
icon.getName().substring(0, icon.getName().lastIndexOf("."));
if (nameWithoutExtension.equals(iconName)) {
iconPath = icon.getAbsolutePath();
break;
}
}
}
}
}
final String packageName = obj.getJSONObject("manifest").getString("package");
final String mainModuleName = module.getName();
boolean isMainModule = false;
if (project.getMainModule() == null) {
String content = FileIOUtils.readFile2String(moduleGradle);
if (content != null && appPattern.matcher(content).find()) {
isMainModule = true;
project.setMainModule(mainModuleName);
project.setMainSourcePath(new File(module, "src/main/java").getAbsolutePath());
}
}
if (iconPath != null) {
project.setIconPath(iconPath);
}
if (packageName != null && isMainModule) {
project.setPackageName(packageName);
}
}
private static File[] listDrawables(File file) {
return listFolderStartingWith(file, "drawable");
}
private static File[] listMipmaps(File file) {
return listFolderStartingWith(file, "mipmap");
}
private static File[] listFolderStartingWith(File file, final String prefix) {
return file.listFiles(
new FileFilter() {
@Override
public boolean accept(File p1) {
return p1.isDirectory() && p1.getName().startsWith(prefix);
}
});
}
private static File[] listFilesNotEndingWith(File file, final String suffix) {
return file.listFiles(
new FileFilter() {
@Override
public boolean accept(File p1) {
return p1.isFile() && !p1.getName().endsWith(suffix);
}
});
}
}
@@ -25,9 +25,6 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;
import com.itsaky.androidide.project.AndroidProject;
import com.itsaky.androidide.project.IDEProject;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
@@ -37,8 +34,6 @@ import java.util.Objects;
public class EditorViewModel extends ViewModel {
private final MutableLiveData<List<File>> mFiles = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<AndroidProject> mProject = new MutableLiveData<>(null);
private final MutableLiveData<IDEProject> mIDEProject = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> mFilesModified = new MutableLiveData<>(false);
/**
@@ -47,44 +42,6 @@ public class EditorViewModel extends ViewModel {
*/
private final MutableLiveData<Pair<Integer, File>> mCurrentFile = new MutableLiveData<>(null);
/**
* Get the current {@link AndroidProject}.
*
* @return The android project.
*/
@Nullable
public AndroidProject getAndroidProject() {
return mProject.getValue();
}
/**
* Set the android project currently opened in the editor activity.
*
* @param project The project that is opened.
*/
public void setAndroidProject(final AndroidProject project) {
this.mProject.setValue(project);
}
/**
* Get the {@link IDEProject} opened in the editor activity.
*
* @return The ide project.
*/
@Nullable
public IDEProject getIDEProject() {
return this.mIDEProject.getValue();
}
/**
* Set the IDEProject that is currently opened in the editor activity.
*
* @param project The project that is opened.
*/
public void setIDEProject(final IDEProject project) {
this.mIDEProject.setValue(project);
}
/**
* Add the given file to the list of opened files.
*
@@ -112,6 +69,10 @@ public class EditorViewModel extends ViewModel {
setCurrentFile(-1, null);
}
public void setCurrentFile(final int index, @Nullable final File file) {
mCurrentFile.setValue(Pair.create(index, file));
}
/**
* Get the opened file at the given index.
*
@@ -167,10 +128,6 @@ public class EditorViewModel extends ViewModel {
return mCurrentFile.getValue().second;
}
public void setCurrentFile(final int index, @Nullable final File file) {
mCurrentFile.setValue(Pair.create(index, file));
}
public void setFilesModified(boolean modified) {
this.mFilesModified.setValue(modified);
}
@@ -62,11 +62,6 @@ import com.itsaky.androidide.utils.TypefaceUtils;
import com.itsaky.inflater.values.ValuesTableFactory;
import com.itsaky.lsp.models.Range;
import io.github.rosemoe.sora.event.ContentChangeEvent;
import io.github.rosemoe.sora.lang.EmptyLanguage;
import io.github.rosemoe.sora.text.Content;
import io.github.rosemoe.sora.widget.component.Magnifier;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.Token;
@@ -78,6 +73,11 @@ import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import io.github.rosemoe.sora.event.ContentChangeEvent;
import io.github.rosemoe.sora.lang.EmptyLanguage;
import io.github.rosemoe.sora.text.Content;
import io.github.rosemoe.sora.widget.component.Magnifier;
/**
* A view that handles opened code editors.
*
+78 -79
View File
@@ -1,101 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:id="@+id/editor_drawerLayout"
android:animateLayoutChanges="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/realContainer">
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/editor_drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/editor_appBarLayout"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/realContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/editor_appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/editor_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/primaryDarkColor"
android:elevation="10dp" />
<com.google.android.material.appbar.MaterialToolbar
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:background="@color/primaryDarkColor"
android:elevation="10dp"
android:id="@+id/editor_toolbar"/>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/build_progress_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:indicatorColor="@color/secondaryColor"/>
<com.itsaky.androidide.views.MaterialBanner
android:id="@+id/syncBanner"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:visibility="gone"/>
app:indicatorColor="@color/secondaryColor" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="@dimen/file_tab_height"
android:background="@color/primaryDarkColor"
app:tabGravity="start"
app:tabIndicatorAnimationMode="elastic"
app:tabIndicatorColor="@color/secondaryColor"
app:tabIndicatorFullWidth="false"
app:tabIndicatorGravity="bottom"
app:tabIndicatorHeight="4dp"
app:tabMode="scrollable"
app:tabSelectedTextColor="@color/secondaryColor"
app:tabTextAppearance="@style/AppTheme.TabLayout.Title"
app:tabTextColor="@color/secondaryTextColor_light" />
<com.itsaky.androidide.views.MaterialBanner
android:id="@+id/syncBanner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="@dimen/file_tab_height"
android:background="@color/primaryDarkColor"
app:tabGravity="start"
app:tabIndicatorAnimationMode="elastic"
app:tabIndicatorColor="@color/secondaryColor"
app:tabIndicatorFullWidth="false"
app:tabIndicatorGravity="bottom"
app:tabIndicatorHeight="4dp"
app:tabMode="scrollable"
app:tabSelectedTextColor="@color/secondaryColor"
app:tabTextAppearance="@style/AppTheme.TabLayout.Title"
app:tabTextColor="@color/secondaryTextColor_light" />
<ViewFlipper
android:id="@+id/editor_container"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="@color/content_background"
android:layout_marginBottom="@dimen/editor_sheet_peek_height"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/>
</com.google.android.material.appbar.AppBarLayout>
<ViewFlipper
android:id="@+id/editor_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/editor_sheet_peek_height"
android:background="@color/content_background"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
<include
android:id="@+id/bottom_sheet"
layout="@layout/layout_editor_bottom_sheet" />
<include
android:id="@+id/bottom_sheet"
layout="@layout/layout_editor_bottom_sheet"/>
<include
android:id="@+id/diagnosticInfo"
layout="@layout/layout_diagnostic_info"/>
layout="@layout/layout_diagnostic_info" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/startNav"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_gravity="start"
android:background="@color/primaryColor"
app:headerLayout="@layout/layout_nav_header"
app:menu="@menu/menu_editor_nav">
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</com.google.android.material.navigation.NavigationView>
<com.google.android.material.navigation.NavigationView
android:id="@+id/startNav"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@color/primaryColor"
app:headerLayout="@layout/layout_nav_header"
app:menu="@menu/menu_editor_nav">
</com.google.android.material.navigation.NavigationView>
<com.google.android.material.navigation.NavigationView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:background="@color/primaryColor">
<com.google.android.material.navigation.NavigationView
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:background="@color/primaryColor"
android:layout_gravity="end">
<FrameLayout
android:id="@+id/editor_frameLayout"
android:layout_height="match_parent"
android:layout_width="match_parent"/>
</com.google.android.material.navigation.NavigationView>
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.google.android.material.navigation.NavigationView>
</androidx.drawerlayout.widget.DrawerLayout>
@@ -14,13 +14,12 @@
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent"
android:elevation="8dp"
android:orientation="vertical"
app:behavior_hideable="false"
app:behavior_peekHeight="@dimen/editor_sheet_peek_height"
app:layout_behavior="com.itsaky.androidide.utils.EditorBottomSheetBehavior">
@@ -29,26 +28,28 @@
android:id="@+id/text_container"
android:layout_width="match_parent"
android:layout_height="56dp"
android:orientation="vertical"
android:background="@android:color/black">
android:background="@android:color/black"
android:orientation="vertical">
<TextView
android:id="@+id/statusText"
android:layout_height="20dp"
android:layout_width="match_parent"
android:layout_height="20dp"
android:gravity="center"
android:textSize="14sp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:singleLine="true"
android:textColor="@color/primaryTextColor"
android:singleLine="true" />
android:textSize="14sp" />
<TextView
android:id="@+id/swipe_hint"
android:layout_width="match_parent"
android:layout_height="18dp"
android:text="@string/msg_swipe_up"
android:gravity="center"
android:text="@string/msg_swipe_up"
android:textColor="@color/secondaryTextColor"
android:textSize="11sp"/>
android:textSize="11sp" />
</LinearLayout>
@@ -56,22 +57,22 @@
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="36dp"
android:background="@android:color/black"
android:layout_below="@id/text_container"
android:background="@android:color/black"
app:tabGravity="fill"
app:tabIndicatorColor="@color/secondaryColor"
app:tabIndicatorGravity="stretch"
app:tabIndicatorHeight="36dp"
app:tabTextAppearance="@style/AppTheme.TabLayout.Title"
app:tabSelectedTextColor="@color/primaryTextColor"
app:tabMode="scrollable"
app:tabGravity="fill"/>
app:tabSelectedTextColor="@color/primaryTextColor"
app:tabTextAppearance="@style/AppTheme.TabLayout.Title" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/content_background"
android:layout_below="@id/tabs"/>
android:layout_below="@id/tabs"
android:background="@color/content_background" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/clear_fab"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

+1 -1
View File
@@ -87,7 +87,7 @@
<string name="preparing_first">Vorbereiten. Der erste Build kann bis zu 10 bis 15 Minuten dauern!</string>
<string name="msg_gradle_terminated">Der Gradle-Prozess wurde mit dem Exit-Code %1$d beendet</string>
<string name="msg_gradlew_installation_failed">Gradle Wrapper konnte nicht installiert werden: %s</string>
<string name="msg_installing_gradlew">Gradle Wrapper ist nicht verf&#xFC;gbar!\nStandard-Wrapper wird installiert. Gradle v7.3 wird bei Bedarf heruntergeladen.</string>
<string name="msg_installing_gradlew">Gradle Wrapper ist nicht verf&#xFC;gbar!\nStandard-Wrapper wird installiert. Gradle v7.4 wird bei Bedarf heruntergeladen.</string>
<string name="msg_getting_daemom_status">Abrufen des Daemon-Status, bitte warten&#x2026;</string>
<string name="msg_daemons_stopped">Gradle-D&#xE4;monen gestoppt</string>
<string name="gradle_daemon_status">Gradle-Daemon-Status</string>
+1 -1
View File
@@ -95,7 +95,7 @@
<string name="preparing_first">Préparation en cours, la première compilation peut prendre de 10&#8211;15 minutes !</string>
<string name="msg_gradle_terminated">Résultat de la compilation : %1$d</string>
<string name="msg_gradlew_installation_failed">Impossible d\'installer Gradle Wrapper: %s</string>
<string name="msg_installing_gradlew">Gradle Wrapper n\'est pas disponible\nInstallation de celui par défaut. Gradle v7.3 sera téléchargé si nécessaire.</string>
<string name="msg_installing_gradlew">Gradle Wrapper n\'est pas disponible\nInstallation de celui par défaut. Gradle v7.4 sera téléchargé si nécessaire.</string>
<string name="msg_getting_daemom_status">Récupération du status du Daemon, veuillez patienter&#8230;</string>
<string name="msg_daemons_stopped">Gradle Daemons arrêté</string>
<string name="gradle_daemon_status">Statut du Daemon Gradle</string>
+1 -1
View File
@@ -89,7 +89,7 @@
<string name="preparing_first">तैयार कर रहे हैं। पहले निर्माण में 10&#8211;15 मिनट तक लग सकते हैं!</string>
<string name="msg_gradle_terminated">ग्रैडल प्रक्रिया एग्जिट कोड %1$d के साथ बाहर निकली</string>
<string name="msg_gradlew_installation_failed">ग्रैडल रैपर इंस्टॉल करने में विफल: %s</string>
<string name="msg_installing_gradlew">ग्रैडल रैपर उपलब्ध नहीं है!\nडिफ़ॉल्ट इंस्टाल किया जा रहा है। यदि आवश्यक हो तो ग्रैडल v7.3 डाउनलोड किया जाएगा।</string>
<string name="msg_installing_gradlew">ग्रैडल रैपर उपलब्ध नहीं है!\nडिफ़ॉल्ट इंस्टाल किया जा रहा है। यदि आवश्यक हो तो ग्रैडल v7.4 डाउनलोड किया जाएगा।</string>
<string name="msg_getting_daemom_status">डेमॉन की स्थिति, कृपया प्रतीक्षा करें&#8230;</string>
<string name="msg_daemons_stopped">ग्रैडल डेमन्स रुक गए</string>
<string name="gradle_daemon_status">ग्रैडल डेमॉन की स्थिति</string>
+1 -1
View File
@@ -90,7 +90,7 @@
<string name="preparing_first">Menyiapkan. Build pertama mungkin memakan waktu sekitar 10&#8211;15 menit!</string>
<string name="msg_gradle_terminated">Proses Gradle berhenti dengan kode exit %1$d</string>
<string name="msg_gradlew_installation_failed">Gagal untuk menginstal Gradle Wrapper: %s</string>
<string name="msg_installing_gradlew">Gradle Wrapper tidak tersedia!\nMenginstal Gradle bawaan. Gradle v7.3 akan diunduh jika diperlukan.</string>
<string name="msg_installing_gradlew">Gradle Wrapper tidak tersedia!\nMenginstal Gradle bawaan. Gradle v7.4 akan diunduh jika diperlukan.</string>
<string name="msg_getting_daemom_status">Mendapatkan status Daemon, mohon tunggu&#8230;</string>
<string name="msg_daemons_stopped">Gradle Daemon Dihentikan</string>
<string name="gradle_daemon_status">Status Gradle Daemon</string>
+1 -1
View File
@@ -89,7 +89,7 @@
<string name="preparing_first">Preparando. A primeira compilação pode levar de 10 a 15 minutos!</string>
<string name="msg_gradle_terminated">Processo Gradle encerrado com código de saída %1$d</string>
<string name="msg_gradlew_installation_failed">Falha ao instalar Gradle Wrapper: %s</string>
<string name="msg_installing_gradlew">Gradle Wrapper não está disponível!\nInstalando um padrão. Gradle v7.3 será baixado se necessário.</string>
<string name="msg_installing_gradlew">Gradle Wrapper não está disponível!\nInstalando um padrão. Gradle v7.4 será baixado se necessário.</string>
<string name="msg_getting_daemom_status">Recebendo o status de Daemon, aguarde…</string>
<string name="msg_daemons_stopped">Gradle Daemons Parado</string>
<string name="gradle_daemon_status">Status do daemon Gradle</string>
+1 -1
View File
@@ -92,7 +92,7 @@
<string name="preparing_first">Выполняется подготовка. Первая сборка может занять 10–15 минут!</string>
<string name="msg_gradle_terminated">Процесс Gradle завершился с кодом %1$d</string>
<string name="msg_gradlew_installation_failed">Ошибка установки Gradle Wrapper: %s</string>
<string name="msg_installing_gradlew">Gradle Wrapper недоступен!\nУстановка версии Gradle по умолчанию. Gradle v7.3 будет скачан, если это необходимо.</string>
<string name="msg_installing_gradlew">Gradle Wrapper недоступен!\nУстановка версии Gradle по умолчанию. Gradle v7.4 будет скачан, если это необходимо.</string>
<string name="msg_getting_daemom_status">Получение статуса Daemon, пожалуйста, подождите&#8230;</string>
<string name="msg_daemons_stopped">Daemon\'ы Gradle остановлены</string>
<string name="gradle_daemon_status">Статус Gradle Daemon</string>
+1 -1
View File
@@ -91,7 +91,7 @@
<string name="preparing_first">准备中。 第一次编译可能耗费 10&#8211;15 分钟!</string>
<string name="msg_gradle_terminated">Gradle进程以代码 %1$d退出</string>
<string name="msg_gradlew_installation_failed">安装Gradle Wrapper失败: %s</string>
<string name="msg_installing_gradlew">Gradle Wrapper不可用!\n正在安装默认版本。 如有需要,Gradle v7.3将会被下载</string>
<string name="msg_installing_gradlew">Gradle Wrapper不可用!\n正在安装默认版本。 如有需要,Gradle v7.4将会被下载</string>
<string name="msg_getting_daemom_status">正在获取Gradle守护进程状态,请稍等&#8230;</string>
<string name="msg_daemons_stopped">Gradle守护进程已停止</string>
<string name="gradle_daemon_status">Gradle守护进程状态</string>
+12 -1
View File
@@ -89,7 +89,7 @@
<string name="preparing_first">Preparing. First build may take up to 10&#8211;15 minutes!</string>
<string name="msg_gradle_terminated">Gradle process exited with exit code %1$d</string>
<string name="msg_gradlew_installation_failed">Failed to install Gradle Wrapper: %s</string>
<string name="msg_installing_gradlew">Gradle Wrapper is not available!\nInstalling default one. Gradle v7.3 will be downloaded if necessary.</string>
<string name="msg_installing_gradlew">Gradle Wrapper is not available!\nInstalling default one. Gradle v7.4 will be downloaded if necessary.</string>
<string name="msg_getting_daemom_status">Getting Daemon status, please wait&#8230;</string>
<string name="msg_daemons_stopped">Gradle Daemons Stopped</string>
<string name="gradle_daemon_status">Gradle daemon status</string>
@@ -357,6 +357,17 @@
<string name="msg_editor_find_case_sensitive">Case sensitive</string>
<string name="msg_editor_find_regex">Regex</string>
<string name="msg_replacing">Replacing...</string>
<string name="title_gradle_service_notification_ticker">Gradle build service started</string>
<string name="title_gradle_service_notification">Gradle build service</string>
<string name="msg_gradle_service_notification">AndroidIDE\'s Gradle build service is running in the background.</string>
<string name="title_cancel_build">Cancel build</string>
<string name="msg_initializing_project">Initializing project</string>
<string name="msg_project_initialized">Project initialized</string>
<string name="msg_project_initialization_failed">Project initialization failed</string>
<string name="msg_project_not_initialized">Project is not initialized</string>
<string name="msg_running_task">Running task: \'%1$s\'</string>
</resources>
+1 -1
View File
@@ -1 +1 @@
build/
/build
+45
View File
@@ -0,0 +1,45 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
}
java {
sourceCompatibility rootProject.ext.javaSourceVersion
targetCompatibility rootProject.ext.javaTargetVersion
}
dependencies {
api project (path: ':build-tools:tooling-api')
}
repositories {
mavenCentral ()
}
compileKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "11"
}
}
@@ -0,0 +1,71 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.builder
import com.itsaky.androidide.tooling.api.messages.result.BuildCancellationRequestResult
import com.itsaky.androidide.tooling.api.messages.result.InitializeResult
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult
import java.util.concurrent.*
/**
* A build service provides API to initialize project, execute builds, query a build, cancel running
* builds, etc.
*
* @author Akash Yadav
*/
interface BuildService {
/**
* Initialize the project.
*
* @param rootDir The root directory of the project to initialize.
* @return A [CompletableFuture] which returns an [InitializeResult] when the project
* initialization process finishes.
*/
fun initializeProject(rootDir: String): CompletableFuture<InitializeResult>
/**
* Execute the given tasks.
*
* @param tasks The tasks to execute. If the fully qualified path of the task is not specified,
* then it will be executed in the root project directory.
* @return A [CompletableFuture] which returns a list of [TaskExecutionResult]. The result
* contains a list of tasks that were executed and the result of the whole execution.
* @see BuildService.executeProjectTasks
*/
fun executeTasks(vararg tasks: String): CompletableFuture<TaskExecutionResult>
/**
* Execute the given tasks of the given project.
*
* @param projectPath The path of the project. All the tasks will be executed in this project.
* @param tasks The tasks to execute. These may or may not be fully qualified names of the
* project. If the task name is not fully qualified, then it will be executed in the given
* project path. For example, if the project path is ':app' and the task is 'assembleDebug',
* then, ':app:assembleDebug' task will be executed.
* @return A [CompletableFuture] which returns a list of [TaskExecutionResult]. The result
* contains a list of tasks that were executed and the result of the whole execution.
*/
fun executeProjectTasks(
projectPath: String,
vararg tasks: String
): CompletableFuture<TaskExecutionResult>
/** Cancel any running build. */
fun cancelCurrentBuild(): CompletableFuture<BuildCancellationRequestResult>
}
@@ -17,35 +17,46 @@
*/
package com.itsaky.androidide.models;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.itsaky.androidide.R;
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
import java.util.Locale;
public class LogLine {
// It makes things easier in LogLanguageImpl
public static final int INFO = SchemeAndroidIDE.LOG_INFO;
public static final int DEBUG = SchemeAndroidIDE.LOG_DEBUG;
public static final int ERROR = SchemeAndroidIDE.LOG_ERROR;
public static final int WARNING = SchemeAndroidIDE.LOG_WARNING;
public static int INFO;
public static int DEBUG;
public static int ERROR;
public static int WARNING;
static {
try {
final var klass =
Class.forName("com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE");
INFO = klass.getField("LOG_INFO").getInt(null);
DEBUG = klass.getField("LOG_DEBUG").getInt(null);
ERROR = klass.getField("LOG_ERROR").getInt(null);
WARNING = klass.getField("LOG_WARNING").getInt(null);
} catch (Throwable err) {
INFO = 0;
DEBUG = 1;
ERROR = 2;
WARNING = 3;
}
}
public String unformatted;
public String date;
public String time;
public String pid;
public String tid;
public String priorityChar;
public String tag;
public String message;
public char priorityChar;
public int priority;
public boolean formatted;
public LogLine(String priorityChar, String tag, String message) {
// For JSONRpc
@SuppressWarnings("unused")
private LogLine() {}
public LogLine(char priorityChar, String tag, String message) {
this("", "", "", "", priorityChar, tag, message);
}
@@ -54,7 +65,7 @@ public class LogLine {
String time,
String pid,
String tid,
String priorityChar,
char priorityChar,
String tag,
String message) {
this(date, time, pid, tid, priorityChar, tag, message, true);
@@ -65,8 +76,8 @@ public class LogLine {
String time,
String pid,
String tid,
String priorityChar,
@NonNull String tag,
char priorityChar,
String tag,
String message,
boolean formatted) {
this.date = date;
@@ -84,11 +95,8 @@ public class LogLine {
}
}
private int parsePriority(String s) {
if (s == null || s.trim().length() > 1) {
return DEBUG;
}
char c = s.toLowerCase(Locale.US).charAt(0);
private int parsePriority(char s) {
final var c = Character.toLowerCase(s);
if (c == 'w') {
return WARNING;
} else if (c == 'e') {
@@ -105,8 +113,7 @@ public class LogLine {
this.formatted = false;
}
@NonNull
public static LogLine forLogString(@NonNull final String log) {
public static LogLine forLogString(final String log) {
try {
final var split = log.split("\\s", 7);
return new LogLine(
@@ -114,7 +121,7 @@ public class LogLine {
split[1], // time
split[2], // process id
split[3], // thread id
split[4], // priority
split[4].charAt(0), // priority
split[5], // tag
split[6] // message
);
@@ -123,35 +130,27 @@ public class LogLine {
}
}
public int getColor(Context ctx) {
int id = R.color.log_normal;
public int getColor() {
int id = 0xffffffff;
if (priority == WARNING) {
id = R.color.log_warning;
id = 0xffff7043;
} else if (priority == ERROR) {
id = R.color.log_error;
id = 0xffc50e29;
} else if (priority == INFO) {
id = R.color.log_info;
id = 0xff4caf50;
}
return ContextCompat.getColor(ctx, id);
return id;
}
@NonNull
@Override
public String toString() {
return this.formatted
? String.format(
"%-6s %-13s %-6s %-6s %-2s %-35s %s",
date, time, pid, tid, priorityChar, trimIfNeeded (tag, 35), message)
date, time, pid, tid, priorityChar, trimIfNeeded(tag, 35), message)
: this.unformatted;
}
public String toSimpleString() {
return this.formatted
? String.format("%-25s %-2s %s", trimIfNeeded(tag, 25), priorityChar, message)
: this.unformatted;
}
@NonNull
private String trimIfNeeded(String tag, int maxLength) {
final var sb = new StringBuilder(tag);
final var length = tag.length();
@@ -166,4 +165,16 @@ public class LogLine {
return sb.toString();
}
public String toSimpleString() {
return this.formatted
? String.format("%-25s %-2s %s", trimIfNeeded(tag, 25), priorityChar, message)
: this.unformatted;
}
public String formattedTagAndMessage() {
return this.formatted
? String.format("%-25s %s", trimIfNeeded(tag, 25), message)
: this.unformatted;
}
}
@@ -103,18 +103,34 @@ public abstract class ILogger {
}
}
public static String priorityChar(int priority) {
public static char priorityChar(int priority) {
switch (priority) {
case INFO:
return "I";
return 'I';
case VERBOSE:
return "V";
return 'V';
case ERROR:
return "E";
return 'E';
case WARNING:
return "W";
return 'W';
default:
return "D";
return 'D';
}
}
public static int priority(char priorityChar) {
switch (priorityChar) {
case 'I':
return INFO;
case 'V':
return VERBOSE;
case 'E':
return ERROR;
case 'W':
return WARNING;
case 'D':
default:
return DEBUG;
}
}
@@ -17,15 +17,7 @@
package com.itsaky.androidide.utils;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.Locale;
import java.util.logging.ConsoleHandler;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import com.itsaky.androidide.models.LogLine;
/**
* {@link ILogger} implementation for the JVM.
@@ -34,91 +26,23 @@ import java.util.logging.Logger;
*/
public class JvmLogger extends ILogger {
private static final Logger LOG = Logger.getLogger("main");
static {
initLogger();
}
public static LogInterceptor interceptor;
protected JvmLogger(String tag) {
super(tag);
}
private static void initLogger() {
LOG.addHandler(new ConsoleHandler());
LOG.setLevel(Level.FINEST);
for (var h : LOG.getHandlers()) {
h.setLevel(Level.FINEST);
h.setFormatter(new LogFormat());
}
}
@Override
protected void doLog(int priority, String message) {
switch (priority) {
case ERROR:
LOG.log(Level.SEVERE, message);
break;
case WARNING:
LOG.log(Level.WARNING, message);
break;
case VERBOSE:
LOG.log(Level.FINEST, message);
break;
case INFO:
LOG.log(Level.INFO, message);
break;
case DEBUG:
default:
LOG.log(Level.FINE, message);
break;
final var log = new LogLine(priorityChar(priority), TAG, message);
if (interceptor != null) {
interceptor.onLog(log);
} else {
System.err.println(log.toSimpleString());
}
}
private static class LogFormat extends Formatter {
private final String format = "%1$tT.%1$tL\t%4$s\t%2$s\t%5$s%6$s%n";
private final Date date = new Date();
@Override
public synchronized String format(LogRecord record) {
date.setTime(record.getMillis());
String source;
if (record.getSourceClassName() != null) {
source = last(record.getSourceClassName());
if (record.getSourceMethodName() != null) {
source += " " + record.getSourceMethodName();
}
} else {
source = record.getLoggerName();
}
var message = formatMessage(record);
var throwable = "";
if (record.getThrown() != null) {
var sw = new StringWriter();
var pw = new PrintWriter(sw);
pw.println();
record.getThrown().printStackTrace(pw);
pw.close();
throwable = sw.toString();
}
return String.format(
Locale.ROOT,
format,
date,
source,
record.getLoggerName(),
record.getLevel().getLocalizedName(),
message,
throwable);
}
private String last(String className) {
var dot = className.lastIndexOf('.');
if (dot == -1) {
return className;
}
return className.substring(dot + 1);
}
public interface LogInterceptor {
void onLog(LogLine line);
}
}
View File
+1 -1
View File
@@ -1 +1 @@
build/
/build
@@ -15,15 +15,16 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.gradle.plugin
plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
}
class IDEModule extends IDEProject {
def buildToolsVersion = 0
def compileSdkVersion = 0
def minSdk = 0
def targetSdk = 0
def versionCode = 0
def versionName = "Not defined"
def isLibrary = true
def viewBindingEnabled = false
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
}
@@ -15,12 +15,9 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.gradle.plugin
package com.itsaky.androidide.tooling.events
class IDEAppModule extends IDEModule {
def applicationId = "Not defined"
public IDEAppModule() {
isLibrary = false
}
}
/** @author Akash Yadav */
interface FinishEvent {
val result: OperationResult
}
@@ -0,0 +1,31 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.tooling.events
/**
* Describes an operation for which an event has occurred.
*
* @author Akash Yadav
*/
abstract class OperationDescriptor {
abstract val name: String
abstract val displayName: String
abstract val parent: OperationDescriptor?
private val gsonType = javaClass.name
}

Some files were not shown because too many files have changed in this diff Show More