diff --git a/app/src/main/java/com/itsaky/androidide/EditorActivity.java b/app/src/main/java/com/itsaky/androidide/EditorActivity.java index c9cf96fcb..a16bfc686 100644 --- a/app/src/main/java/com/itsaky/androidide/EditorActivity.java +++ b/app/src/main/java/com/itsaky/androidide/EditorActivity.java @@ -119,6 +119,7 @@ import java.util.Stack; import java.util.regex.Pattern; import me.piruin.quickaction.ActionItem; import me.piruin.quickaction.QuickAction; +import com.itsaky.androidide.models.project.IDEProject; public class EditorActivity extends StudioActivity implements FileTreeFragment.FileActionListener, IDEService.BuildListener, @@ -292,7 +293,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F } private void startServices() { - getBuildService().showDependencies(); + getBuildService().initProject(); } private void removeFromParent(View v) { @@ -702,13 +703,18 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F .setPositiveButton(android.R.string.yes, (p1, p2) -> { p1.dismiss(); if(getBuildService() != null) - getBuildService().showDependencies(); + getBuildService().initProject(); }) .setTitle(R.string.failed) .setMessage(R.string.msg_first_prepare_failed) .create().show(); } + @Override + public void onProjectLoaded(IDEProject project) { + + } + @Override public void saveFiles() { saveAll(); diff --git a/app/src/main/java/com/itsaky/androidide/SplashActivity.java b/app/src/main/java/com/itsaky/androidide/SplashActivity.java index 0af992bc7..905000ec8 100644 --- a/app/src/main/java/com/itsaky/androidide/SplashActivity.java +++ b/app/src/main/java/com/itsaky/androidide/SplashActivity.java @@ -34,8 +34,7 @@ public class SplashActivity extends StudioActivity } @Override - protected void onCreate(Bundle savedInstanceState) - { + protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding.splashText.setText(getString(R.string.msg_checking_storage_permissions)); } diff --git a/app/src/main/java/com/itsaky/androidide/managers/ToolsManager.java b/app/src/main/java/com/itsaky/androidide/managers/ToolsManager.java index bc8d66c9b..e8781a350 100644 --- a/app/src/main/java/com/itsaky/androidide/managers/ToolsManager.java +++ b/app/src/main/java/com/itsaky/androidide/managers/ToolsManager.java @@ -16,7 +16,7 @@ import net.lingala.zip4j.ZipFile; public class ToolsManager { private static PreferenceManager prefs; - + public static final int JLS_VERSION = 1; public static final int LOGSENDER_VERSION = 1; public static final int CLEANER_VERSION = 1; @@ -37,10 +37,16 @@ public class ToolsManager { writeInitScript(); writeBashrc(); + rewriteProjectData(); + if(onFinish != null) onFinish.run(); } + private static void rewriteProjectData() { + FileIOUtils.writeFileFromString(Environment.PROJECT_DATA_FILE, "/**********************/"); + } + private static void copyBusyboxIfNeeded() { File exec = Environment.BUSYBOX; if(exec.exists()) return; @@ -111,7 +117,7 @@ public class ToolsManager { "PS1=\'\\[\\e[0;34m\\]\\w\\[\\e[0m\\] \\[\\e[0;97m\\]$\\[\\e[0m\\] \'"; private static final String INIT_SCRIPT = - "import org.gradle.api.Plugin" + ( "import org.gradle.api.Plugin" + "\nimport org.gradle.api.Project" + "\nimport groovy.json.*" + "\n" @@ -123,117 +129,6 @@ public class ToolsManager { + "\n }" + "\n}" + "\n" - + "\ngradle.projectsEvaluated {" - + "\n IDEProject project = new IDEProject()" - + "\n project.name = rootProject.getName()" - + "\n project.displayName = rootProject.getDisplayName()" - + "\n project.description = rootProject.getDescription()" - + "\n project.path = rootProject.getPath()" - + "\n " - + "\n def tasks = rootProject.tasks" - + "\n if(tasks != null && tasks.size() > 0) {" - + "\n addTasks(project, rootProject)" - + "\n }" - + "\n " - + "\n rootProject.subprojects.forEach { sub ->" - + "\n addIDEProject(project, sub);" - + "\n }" - + "\n " - + "\n File out = new File(\"" + Environment.PROJECT_DATA_FILE.getAbsolutePath() + "\")" - + "\n out << new JsonBuilder( project ).toPrettyString()" - + "\n " - + "\n println \">>> PROJECT INITIALIZED <<<\"" - + "\n}" - + "\n" - + "\ndef addIDEProject (IDEProject parent, Project sub) {" - + "\n if(parent == null || sub == null) {" - + "\n return" - + "\n }" - + "\n " - + "\n def isApp = sub.getPluginManager().hasPlugin(\"com.android.application\")" - + "\n def isLibrary = sub.getPluginManager().hasPlugin(\"com.android.library\")" - + "\n " - + "\n if(!(isApp || isLibrary)) {" - + "\n return" - + "\n }" - + "\n " - + "\n IDEModule module = new IDEModule()" - + "\n if(isApp) {" - + "\n module = new IDEAppModule()" - + "\n }" - + "\n " - + "\n def android = sub.android" - + "\n def config = android.defaultConfig" - + "\n " - + "\n module.name = sub.getName()" - + "\n module.displayName = sub.getDisplayName()" - + "\n module.description = sub.getDescription()" - + "\n module.path = sub.getPath()" - + "\n module.buildToolsVersion = android.buildToolsVersion" - + "\n module.compileSdkVersion = android.compileSdkVersion" - + "\n module.minSdk = config.minSdkVersion" - + "\n module.targetSdk = config.targetSdkVersion" - + "\n module.versionCode = config.versionCode" - + "\n module.versionName = config.versionName" - + "\n " - + "\n if(isApp) {" - + "\n module.applicationId = config.applicationId" - + "\n }" - + "\n " - + "\n def tasks = sub.tasks" - + "\n if(tasks != null && tasks.size() > 0) {" - + "\n addTasks(module, sub)" - + "\n }" - + "\n " - + "\n def variants = null" - + "\n if(isApp) {" - + "\n variants = project.android.applicationVariants" - + "\n } else if (isLibrary) {" - + "\n variants = project.android.libraryVariants" - + "\n } else {" - + "\n println \"AndroidIDE: Cannot build variants of project: ${module.name}\"" - + "\n }" - + "\n " - + "\n if(variants != null) {" - + "\n variants.all { variant ->" - + "\n addDependencies(module, variant);" - + "\n }" - + "\n }" - + "\n " - + "\n def subs = sub.getSubprojects()" - + "\n if(subs != null && subs.size() > 0) {" - + "\n subs.forEach { subOfSub -> " - + "\n addIDEProject(module, subOfSub)" - + "\n }" - + "\n }" - + "\n parent.modules.add(module)" - + "\n}" - + "\n" - + "\ndef addDependencies(def module, def variant) {" - + "\n variant.getCompileClasspath().each { dependency ->" - + "\n def path = dependency.absolutePath" - + "\n if(!module.dependencies.contains(path)) {" - + "\n module.dependencies.add(dependency.absolutePath);" - + "\n }" - + "\n }" - + "\n}" - + "\n" - + "\ndef addTasks (IDEProject project, Project gradleProject) {" - + "\n if(project == null || gradleProject == null) {" - + "\n return" - + "\n }" - + "\n " - + "\n gradleProject.tasks.forEach { gradleTask -> " - + "\n IDETask task = new IDETask()" - + "\n task.name = gradleTask.getName()" - + "\n task.description = gradleTask.getDescription()" - + "\n task.group = gradleTask.getGroup()" - + "\n task.path = gradleTask.getPath()" - + "\n " - + "\n project.tasks.add(task)" - + "\n }" - + "\n}" - + "\n" + "\nclass AndroidIDEPlugin implements Plugin {" + "\n " + "\n def AAPT2_VERSION = \"4.0.1-6197926\"" @@ -258,7 +153,7 @@ public class ToolsManager { + "\n " + "\n project.configurations.matching { it.name == \'_internal_aapt2_binary\' }.all { config ->" + "\n config.resolutionStrategy.eachDependency { details ->" - + "\n details.useVersion(\"$AAPT2_VERSION\")" + + "\n details.useVersion(\"${AAPT2_VERSION}\")" + "\n }" + "\n }" + "\n " @@ -272,8 +167,49 @@ public class ToolsManager { + "\n return" + "\n }" + "\n " + + "\n project.tasks.create(" + + "\n name: \"initializeIDEProject\"," + + "\n group: \"AndroidIDE\"," + + "\n description: \"Initializes the project in AndroidIDE. AndroidIDE manages the proper execution of this task. You\'re not supposed to execute this task manually!\"" + + "\n )" + + "\n {" + + "\n outputs.upToDateWhen { false }" + + "\n dependsOn assembleDebug" + + "\n " + + "\n doLast {" + + "\n def root = project.getRootProject()" + + "\n " + + "\n IDEProject ideProject = new IDEProject()" + + "\n ideProject.name = root.getName()" + + "\n ideProject.displayName = root.getDisplayName()" + + "\n ideProject.description = root.getDescription()" + + "\n ideProject.path = root.getPath()" + + "\n " + + "\n def tasks = root.tasks" + + "\n if(tasks != null && tasks.size() > 0) {" + + "\n addTasks(ideProject, root)" + + "\n }" + + "\n " + + "\n root.subprojects.forEach { sub ->" + + "\n addIDEProject(ideProject, sub);" + + "\n }" + + "\n " + + "\n File out = new File(\"@@PROJECT_DATA_FILE@@\")" + + "\n out << new JsonBuilder( ideProject ).toPrettyString()" + + "\n " + + "\n println \">>> PROJECT INITIALIZED <<<\"" + + "\n }" + + "\n }" + + "\n " + + "\n project.afterEvaluate {" + + "\n project.tasks.getByName(\'assembleDebug\').finalizedBy(\'initializeIDEProject\')" + + "\n project.tasks.getByName(\'assembleRelease\').finalizedBy(\'initializeIDEProject\')" + + "\n }" + + "\n " + "\n variants.all { variant ->" - + "\n project.tasks.create(name: \"printDependencies${variant.name.capitalize()}\", group: \"Android IDE\", description: \"[AndroidIDE] List all dependencies in project including those in subprojects for ${variant.name.capitalize()} build.\") {" + + "\n " + + "\n def variantName = variant.name.capitalize()" + + "\n project.tasks.create(name: \"printDependencies${variantName}\", group: \"AndroidIDE\", description: \"List all dependencies in project including those in subprojects for ${variant.name.capitalize()} build.\") {" + "\n doLast {" + "\n printDependencies(project, variant)" + "\n }" @@ -281,6 +217,95 @@ public class ToolsManager { + "\n }" + "\n }" + "\n " + + "\n def addIDEProject (IDEProject parent, Project sub) {" + + "\n if(parent == null || sub == null) {" + + "\n return" + + "\n }" + + "\n " + + "\n def isApp = sub.getPluginManager().hasPlugin(\"com.android.application\")" + + "\n def isLibrary = sub.getPluginManager().hasPlugin(\"com.android.library\")" + + "\n " + + "\n if(!(isApp || isLibrary)) {" + + "\n return" + + "\n }" + + "\n " + + "\n IDEModule module = new IDEModule()" + + "\n if(isApp) {" + + "\n module = new IDEAppModule()" + + "\n }" + + "\n " + + "\n def android = sub.android" + + "\n def config = android.defaultConfig" + + "\n " + + "\n module.name = sub.getName()" + + "\n module.displayName = sub.getDisplayName()" + + "\n module.description = sub.getDescription()" + + "\n module.path = sub.getPath()" + + "\n module.buildToolsVersion = android.buildToolsVersion" + + "\n module.compileSdkVersion = android.compileSdkVersion" + + "\n module.minSdk = config.minSdkVersion" + + "\n module.targetSdk = config.targetSdkVersion" + + "\n module.versionCode = config.versionCode" + + "\n module.versionName = config.versionName" + + "\n " + + "\n if(isApp) {" + + "\n module.applicationId = config.applicationId" + + "\n }" + + "\n " + + "\n def tasks = sub.tasks" + + "\n if(tasks != null && tasks.size() > 0) {" + + "\n addTasks(module, sub)" + + "\n }" + + "\n " + + "\n def variants = null" + + "\n if(isApp) {" + + "\n variants = sub.android.applicationVariants" + + "\n } else if (isLibrary) {" + + "\n variants = sub.android.libraryVariants" + + "\n } else {" + + "\n println \"AndroidIDE: Cannot find build variants of project: ${module.name}(${module.path})\"" + + "\n }" + + "\n " + + "\n if(variants != null) {" + + "\n variants.all { variant ->" + + "\n addDependencies(module, variant);" + + "\n }" + + "\n }" + + "\n " + + "\n def subs = sub.getSubprojects()" + + "\n if(subs != null && subs.size() > 0) {" + + "\n subs.forEach { subOfSub -> " + + "\n addIDEProject(module, subOfSub)" + + "\n }" + + "\n }" + + "\n parent.modules.add(module)" + + "\n }" + + "\n " + + "\n def addDependencies(def module, def variant) {" + + "\n variant.getCompileClasspath().each { dependency ->" + + "\n def path = dependency.absolutePath" + + "\n if(!module.dependencies.contains(path)) {" + + "\n module.dependencies.add(dependency.absolutePath);" + + "\n }" + + "\n }" + + "\n }" + + "\n " + + "\n def addTasks (IDEProject project, Project gradleProject) {" + + "\n if(project == null || gradleProject == null) {" + + "\n return" + + "\n }" + + "\n " + + "\n gradleProject.tasks.forEach { gradleTask -> " + + "\n IDETask task = new IDETask()" + + "\n task.name = gradleTask.getName()" + + "\n task.description = gradleTask.getDescription()" + + "\n task.group = gradleTask.getGroup()" + + "\n task.path = gradleTask.getPath()" + + "\n " + + "\n project.tasks.add(task)" + + "\n }" + + "\n }" + + "\n " + "\n def printDependencies(def project, def variant) {" + "\n println \">> DEPENDENCIES START <<\"" + "\n variant.getCompileClasspath().each { dep ->" @@ -324,7 +349,7 @@ public class ToolsManager { + "\n def description = \"Not defined\"" + "\n def group = \"Not defined\"" + "\n def path = \"Not defined\"" - + "\n}"; + + "\n}").replace("@@PROJECT_DATA_FILE@@", Environment.PROJECT_DATA_FILE.getAbsolutePath()); private static final Logger LOG = Logger.instance("ToolsManager"); } diff --git a/app/src/main/java/com/itsaky/androidide/models/project/IDEProject.java b/app/src/main/java/com/itsaky/androidide/models/project/IDEProject.java index 666ebc179..24b348812 100644 --- a/app/src/main/java/com/itsaky/androidide/models/project/IDEProject.java +++ b/app/src/main/java/com/itsaky/androidide/models/project/IDEProject.java @@ -12,27 +12,27 @@ public class IDEProject { * Name of this project */ @SerializedName("name") - public String name; + public String name = ""; /** * Name of this project which should be displayed to user */ @SerializedName("displayName") - public String displayName; + public String displayName = ""; /** * Description of this project. Maybe null. */ @SerializedName("description") - public String description; + public String description = ""; /** * Path of this project. This is NOT the directory path of this project. * - * For example, ':app: or ':module:subModule' + * For example, ':app or ':module:subModule' */ @SerializedName("path") - public String path; + public String path = ""; /** * Subprojects of this project diff --git a/app/src/main/java/com/itsaky/androidide/services/IDEService.java b/app/src/main/java/com/itsaky/androidide/services/IDEService.java index 3e03548c3..fbb2019ff 100644 --- a/app/src/main/java/com/itsaky/androidide/services/IDEService.java +++ b/app/src/main/java/com/itsaky/androidide/services/IDEService.java @@ -21,6 +21,15 @@ import java.util.Date; import java.util.List; import java.util.Locale; import org.json.JSONObject; +import com.itsaky.androidide.models.project.IDEProject; +import com.itsaky.androidide.tasks.TaskExecutor; +import com.itsaky.androidide.tasks.callables.ReadFileTask; +import com.itsaky.androidide.utils.RuntimeTypeAdapterFactory; +import com.itsaky.androidide.models.project.IDEModule; +import com.itsaky.androidide.models.project.IDEAppModule; +import com.google.gson.GsonBuilder; +import com.blankj.utilcode.util.FileIOUtils; +import com.itsaky.androidide.utils.Logger; public class IDEService implements ShellServer.Callback { @@ -29,21 +38,19 @@ public class IDEService implements ShellServer.Callback { private GradleTask currentTask; private StudioApp app; + private IDEProject mIDEProject; private AndroidProject project; private List dependencies; private boolean isBuilding = false; private boolean isRunning = false; - private boolean outputtingJars = false; private boolean recreateShellOnDone = false; private final String RUN_TASK = "> Task"; private final String STARTING_DAEMON = "Starting a "; private final String BUILD_SUCCESS = "BUILD SUCCESSFUL"; private final String BUILD_FAILED = "BUILD FAILED"; - private final String SHOW_DEPENDENCIES_START = ">> DEPENDENCIES START <<"; - private final String SHOW_DEPENDENCIES_END = ">> DEPENDENCIES END <<"; private final String PROJECT_INITIALIZED = ">>> PROJECT INITIALIZED <<<"; public static final int TASK_SHOW_DEPENDENCIES = GradleTask.TASK_SHOW_DEPENDENCIES; @@ -59,13 +66,15 @@ public class IDEService implements ShellServer.Callback { 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 Logger LOG = Logger.instance("IDEService"); + public IDEService(AndroidProject project) { this.app = StudioApp.getInstance(); this.project = project; this.shell = app.newShell(this); - isRunning = true; + this.isRunning = true; } public IDEService setListener(BuildListener listener) { @@ -87,42 +96,7 @@ public class IDEService implements ShellServer.Callback { } boolean shouldOutput = true; - if(line.contains(SHOW_DEPENDENCIES_START)) { - shouldOutput = false; - dependencies = new ArrayList<>(); - outputtingJars = true; - if(listener != null) - listener.onRunTask(currentTask, "> Task :app:startCompletionServices"); - return; - } else if(line.contains(SHOW_DEPENDENCIES_END)) { - shouldOutput = false; - outputtingJars = false; - if(listener != null - && dependencies != null) { - - listener.onGetDependencies(dependencies); - } - StudioApp.getInstance().getPrefManager().putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false); - return; - } - - if(outputtingJars) { - JSONObject o = asObjectOrNull(line); - try { - if(o.has("type")) { - String type = o.getString("type"); - if(type.equals("ANDROID_JAR")) { - shouldOutput = false; - Environment.setBootClasspath(new File(o.getString("requested"))); - } - } else if(o.has("file")) { - shouldOutput = false; - String jar = o.getString("file"); - - dependencies.add(jar); - } - } catch (Throwable th) {} - } + if(shouldOutput) { String text = line.replace(StudioApp.getInstance().getRootDir().getAbsolutePath(), "ANDROIDIDE_HOME"); listener.appendOutput(currentTask, text); @@ -135,7 +109,6 @@ public class IDEService implements ShellServer.Callback { } else if(charSequence.toString().contains(BUILD_SUCCESS)) { isBuilding = false; - outputtingJars = false; if(recreateShellOnDone) createShell(); if(currentTask != null && currentTask.getTaskID() != TASK_SHOW_DEPENDENCIES) @@ -144,7 +117,6 @@ public class IDEService implements ShellServer.Callback { } else if(charSequence.toString().contains(BUILD_FAILED)) { isBuilding = false; - outputtingJars = false; if(recreateShellOnDone) createShell(); if(currentTask != null) { @@ -159,6 +131,27 @@ public class IDEService implements ShellServer.Callback { } private void readIdeProject() { + // Parsing project data is meaningless if there is no one listening... + if(listener == null) + return; + + final BuildListener local = listener; + new Thread(() -> { + RuntimeTypeAdapterFactory typeAdapter = RuntimeTypeAdapterFactory.of(IDEProject.class) + .registerSubtype(IDEProject.class, "ide_project") + .registerSubtype(IDEModule.class, "ide_module") + .registerSubtype(IDEAppModule.class, "ide_app_module"); + + IDEProject ideProject = new GsonBuilder() + .registerTypeAdapterFactory(typeAdapter) + .create() + .fromJson( + FileIOUtils.readFile2String(Environment.PROJECT_DATA_FILE), + IDEProject.class + ); + if(local != null) local.onProjectLoaded(ideProject); + LOG.i("Deserialized project: " + ideProject, (ideProject != null ? String.format("Name: %s\nPath: %s\nTask count: %d\nSubproject count:%s", ideProject.name, ideProject.path, ideProject.tasks.size(), ideProject.modules.size()) : "")); + }).start(); } private void appendOutputSeparator() { @@ -256,8 +249,8 @@ public class IDEService implements ShellServer.Callback { } } - public void showDependencies() { - execTask(BaseGradleTasks.SHOW_DEPENDENCIES); + public void initProject() { + execTask(BaseGradleTasks.INITIALIZE_IDE_PROJECT); } public void assembleDebug(boolean installApk) { @@ -343,6 +336,7 @@ public class IDEService implements ShellServer.Callback { } public static interface BuildListener { + void onProjectLoaded(IDEProject project); void onGetDependencies(List dependencies); void onGetDependenciesFailed(); diff --git a/app/src/main/java/com/itsaky/androidide/tasks/GradleTask.java b/app/src/main/java/com/itsaky/androidide/tasks/GradleTask.java index 3f269eeaf..636a082b0 100644 --- a/app/src/main/java/com/itsaky/androidide/tasks/GradleTask.java +++ b/app/src/main/java/com/itsaky/androidide/tasks/GradleTask.java @@ -37,5 +37,5 @@ public interface GradleTask { 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; } diff --git a/app/src/main/java/com/itsaky/androidide/tasks/gradle/BaseGradleTasks.java b/app/src/main/java/com/itsaky/androidide/tasks/gradle/BaseGradleTasks.java index 4c2aff168..975dd4b94 100644 --- a/app/src/main/java/com/itsaky/androidide/tasks/gradle/BaseGradleTasks.java +++ b/app/src/main/java/com/itsaky/androidide/tasks/gradle/BaseGradleTasks.java @@ -14,7 +14,7 @@ import com.itsaky.androidide.tasks.gradle.dex.MergeLibAndProjectDex; 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.PrintDependencies; +import com.itsaky.androidide.tasks.gradle.plugin.InitializeIDEProject; public class BaseGradleTasks { @@ -36,5 +36,5 @@ public class BaseGradleTasks { MERGE_EXT_DEX = new MergeExtDex(), MERGE_LIB_AND_PROJECT_DEX = new MergeLibAndProjectDex(), - SHOW_DEPENDENCIES = new PrintDependencies(); + INITIALIZE_IDE_PROJECT = new InitializeIDEProject(); } diff --git a/app/src/main/java/com/itsaky/androidide/tasks/gradle/plugin/InitializeIDEProject.java b/app/src/main/java/com/itsaky/androidide/tasks/gradle/plugin/InitializeIDEProject.java new file mode 100644 index 000000000..ba658ff2f --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/tasks/gradle/plugin/InitializeIDEProject.java @@ -0,0 +1,50 @@ +package com.itsaky.androidide.tasks.gradle.plugin; + +import com.itsaky.androidide.tasks.BaseGradleTask; +import com.itsaky.androidide.tasks.GradleTask; +import java.io.File; +import java.util.List; +import java.util.Arrays; + +public class InitializeIDEProject extends BaseGradleTask { + + @Override + public String getName() { + return "initializeIDEProject"; + } + + @Override + public String getCommands() { + return "initializeIDEProject"; + } + + @Override + public List getTasks() { + return Arrays.asList(getCommands()); + } + + @Override + public int getTaskID() { + return TASK_INIT_PROJECT; + } + + @Override + public boolean canOutput() { + return false; + } + + @Override + public boolean buildsApk() { + return false; + } + + @Override + public File getApk(String buildFolder, String moduleName) { + return null; + } + + @Override + public GradleTask.Type getType() { + return Type.ANDROIDIDE_PLUGIN; + } +} diff --git a/app/src/main/java/com/itsaky/androidide/tasks/gradle/plugin/PrintDependencies.java b/app/src/main/java/com/itsaky/androidide/tasks/gradle/plugin/PrintDependencies.java deleted file mode 100644 index cc53891c3..000000000 --- a/app/src/main/java/com/itsaky/androidide/tasks/gradle/plugin/PrintDependencies.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.itsaky.androidide.tasks.gradle.plugin; - -import com.itsaky.androidide.app.StudioApp; -import com.itsaky.androidide.managers.PreferenceManager; -import com.itsaky.androidide.services.IDEService; -import com.itsaky.androidide.tasks.BaseGradleTask; -import java.io.File; -import java.util.List; -import java.util.ArrayList; - -public class PrintDependencies extends BaseGradleTask { - - @Override - public List getTasks() { - List tasks = new ArrayList<>(); - for(String task : getCommands().split("\\s")) { - if(task != null && task.trim().length() > 0) - tasks.add(task); - } - return tasks; - } - - @Override - public String getName() { - return "Start Completion Services"; - } - - @Override - public String getCommands() { - return (StudioApp.getInstance().getPrefManager().getBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, true) ? "assembleDebug " : "") + "printDependenciesDebug"; - } - - @Override - public int getTaskID() { - return IDEService.TASK_SHOW_DEPENDENCIES; - } - - @Override - public boolean canOutput() { - return true; - } - - @Override - public boolean buildsApk() { - return false; - } - - @Override - public File getApk(String buildFolder, String moduleName) { - return null; - } - - @Override - public Type getType() { - return Type.ANDROIDIDE_PLUGIN; - } -} diff --git a/app/src/main/java/com/itsaky/androidide/utils/ProjectWriter.java b/app/src/main/java/com/itsaky/androidide/utils/ProjectWriter.java index 846c668c4..0bc14d7aa 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/ProjectWriter.java +++ b/app/src/main/java/com/itsaky/androidide/utils/ProjectWriter.java @@ -11,8 +11,8 @@ import com.itsaky.androidide.interfaces.ProjectWriterCallback; import com.itsaky.androidide.models.NewProjectDetails; import com.itsaky.androidide.models.ProjectTemplate; import java.io.File; -import java.util.regex.Pattern; import java.util.regex.Matcher; +import java.util.regex.Pattern; public class ProjectWriter { @@ -31,9 +31,7 @@ public class ProjectWriter { MSDK = "$min_sdk", TSDK = "$target_sdk", ANDROIDIDE_PLUGIN = "$androidide_gradle_plugin_version"; - - private static final String ANDROIDIDE_PLUGIN_VERSION = "1.0.6"; - + public static String createMenu() { return " {@code + * abstract class Shape { + * int x; + * int y; + * } + * class Circle extends Shape { + * int radius; + * } + * class Rectangle extends Shape { + * int width; + * int height; + * } + * class Diamond extends Shape { + * int width; + * int height; + * } + * class Drawing { + * Shape bottomShape; + * Shape topShape; + * } + * } + *

Without additional type information, the serialized JSON is ambiguous. Is + * the bottom shape in this drawing a rectangle or a diamond?

   {@code
+ *   {
+ *     "bottomShape": {
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized:
   {@code
+ *   {
+ *     "bottomShape": {
+ *       "type": "Diamond",
+ *       "width": 10,
+ *       "height": 5,
+ *       "x": 0,
+ *       "y": 0
+ *     },
+ *     "topShape": {
+ *       "type": "Circle",
+ *       "radius": 2,
+ *       "x": 4,
+ *       "y": 1
+ *     }
+ *   }}
+ * Both the type field name ({@code "type"}) and the type labels ({@code + * "Rectangle"}) are configurable. + * + *

Registering Types

+ * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field + * name to the {@link #of} factory method. If you don't supply an explicit type + * field name, {@code "type"} will be used.
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory
+ *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+ * }
+ * Next register all of your subtypes. Every subtype must be explicitly + * registered. This protects your application from injection attacks. If you + * don't supply an explicit type label, the type's simple name will be used. + *
   {@code
+ *   shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ *   shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ *   shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+ * }
+ * Finally, register the type adapter factory in your application's GSON builder: + *
   {@code
+ *   Gson gson = new GsonBuilder()
+ *       .registerTypeAdapterFactory(shapeAdapterFactory)
+ *       .create();
+ * }
+ * Like {@code GsonBuilder}, this API supports chaining:
   {@code
+ *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ *       .registerSubtype(Rectangle.class)
+ *       .registerSubtype(Circle.class)
+ *       .registerSubtype(Diamond.class);
+ * }
+ * + *

Serialization and deserialization

+ * In order to serialize and deserialize a polymorphic object, + * you must specify the base type explicitly. + *
   {@code
+ *   Diamond diamond = new Diamond();
+ *   String json = gson.toJson(diamond, Shape.class);
+ * }
+ * And then: + *
   {@code
+ *   Shape shape = gson.fromJson(json, Shape.class);
+ * }
+ */ +public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { + private final Class baseType; + private final String typeFieldName; + private final Map> labelToSubtype = new LinkedHashMap>(); + private final Map, String> subtypeToLabel = new LinkedHashMap, String>(); + private final boolean maintainType; + + private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName, boolean maintainType) { + if (typeFieldName == null || baseType == null) { + throw new NullPointerException(); + } + this.baseType = baseType; + this.typeFieldName = typeFieldName; + this.maintainType = maintainType; + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + * {@code maintainType} flag decide if the type will be stored in pojo or not. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { + return new RuntimeTypeAdapterFactory(baseType, typeFieldName, maintainType); + } + + /** + * Creates a new runtime type adapter using for {@code baseType} using {@code + * typeFieldName} as the type field name. Type field names are case sensitive. + */ + public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { + return new RuntimeTypeAdapterFactory(baseType, typeFieldName, false); + } + + /** + * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as + * the type field name. + */ + public static RuntimeTypeAdapterFactory of(Class baseType) { + return new RuntimeTypeAdapterFactory(baseType, "type", false); + } + + /** + * Registers {@code type} identified by {@code label}. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either {@code type} or {@code label} + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { + if (type == null || label == null) { + throw new NullPointerException(); + } + if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { + throw new IllegalArgumentException("types and labels must be unique"); + } + labelToSubtype.put(label, type); + subtypeToLabel.put(type, label); + return this; + } + + /** + * Registers {@code type} identified by its {@link Class#getSimpleName simple + * name}. Labels are case sensitive. + * + * @throws IllegalArgumentException if either {@code type} or its simple name + * have already been registered on this type adapter. + */ + public RuntimeTypeAdapterFactory registerSubtype(Class type) { + return registerSubtype(type, type.getSimpleName()); + } + + public TypeAdapter create(Gson gson, TypeToken type) { + if (type.getRawType() != baseType) { + return null; + } + + final Map> labelToDelegate + = new LinkedHashMap>(); + final Map, TypeAdapter> subtypeToDelegate + = new LinkedHashMap, TypeAdapter>(); + for (Map.Entry> entry : labelToSubtype.entrySet()) { + TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); + labelToDelegate.put(entry.getKey(), delegate); + subtypeToDelegate.put(entry.getValue(), delegate); + } + + return new TypeAdapter() { + @Override public R read(JsonReader in) throws IOException { + JsonElement jsonElement = Streams.parse(in); + JsonElement labelJsonElement; + if (maintainType) { + labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); + } else { + labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); + } + + if (labelJsonElement == null) { + throw new JsonParseException("cannot deserialize " + baseType + + " because it does not define a field named " + typeFieldName); + } + String label = labelJsonElement.getAsString(); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); + if (delegate == null) { + throw new JsonParseException("cannot deserialize " + baseType + " subtype named " + + label + "; did you forget to register a subtype?"); + } + return delegate.fromJsonTree(jsonElement); + } + + @Override public void write(JsonWriter out, R value) throws IOException { + Class srcType = value.getClass(); + String label = subtypeToLabel.get(srcType); + @SuppressWarnings("unchecked") // registration requires that subtype extends T + TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); + if (delegate == null) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + "; did you forget to register a subtype?"); + } + JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); + + if (maintainType) { + Streams.write(jsonObject, out); + return; + } + + JsonObject clone = new JsonObject(); + + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } + clone.add(typeFieldName, new JsonPrimitive(label)); + + for (Map.Entry e : jsonObject.entrySet()) { + clone.add(e.getKey(), e.getValue()); + } + Streams.write(clone, out); + } + }.nullSafe(); + } +}